@applicaster/zapp-react-native-ui-components 14.0.0-rc.9 → 15.0.0-rc.1

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.
Files changed (163) hide show
  1. package/Components/AnimatedInOut/index.tsx +5 -3
  2. package/Components/AudioPlayer/index.tsx +15 -0
  3. package/Components/AudioPlayer/mobile/Layout.tsx +66 -0
  4. package/Components/AudioPlayer/{__tests__/__snapshots__/audioPlayer.test.js.snap → mobile/__tests__/__snapshots__/audioPlayerMobileLayout.test.js.snap} +2 -2
  5. package/Components/AudioPlayer/mobile/__tests__/audioPlayerMobileLayout.test.js +18 -0
  6. package/Components/AudioPlayer/mobile/index.tsx +18 -0
  7. package/Components/AudioPlayer/{Artwork.tsx → tv/Artwork.tsx} +3 -2
  8. package/Components/AudioPlayer/{Channel.tsx → tv/Channel.tsx} +7 -7
  9. package/Components/AudioPlayer/tv/Layout.tsx +168 -0
  10. package/Components/AudioPlayer/{Runtime.tsx → tv/Runtime.tsx} +7 -1
  11. package/Components/AudioPlayer/{Summary.tsx → tv/Summary.tsx} +6 -2
  12. package/Components/AudioPlayer/{Title.tsx → tv/Title.tsx} +6 -2
  13. package/Components/AudioPlayer/{__tests__ → tv/__tests__}/__snapshots__/Runtime.test.js.snap +2 -2
  14. package/Components/AudioPlayer/tv/__tests__/__snapshots__/audioPlayer.test.js.snap +164 -0
  15. package/Components/AudioPlayer/tv/__tests__/__snapshots__/channel.test.js.snap +19 -0
  16. package/Components/AudioPlayer/{__tests__ → tv/__tests__}/__snapshots__/summary.test.js.snap +1 -2
  17. package/Components/AudioPlayer/{__tests__ → tv/__tests__}/__snapshots__/title.test.js.snap +1 -2
  18. package/Components/AudioPlayer/{__tests__ → tv/__tests__}/audioPlayer.test.js +7 -3
  19. package/Components/AudioPlayer/{helpers.tsx → tv/helpers.tsx} +11 -5
  20. package/Components/AudioPlayer/{AudioPlayer.tsx → tv/index.tsx} +17 -58
  21. package/Components/AudioPlayer/types.ts +40 -0
  22. package/Components/BaseFocusable/index.tsx +23 -12
  23. package/Components/Cell/Cell.tsx +91 -64
  24. package/Components/Cell/CellWithFocusable.tsx +3 -0
  25. package/Components/Cell/__tests__/CellWIthFocusable.test.js +3 -2
  26. package/Components/Cell/index.js +7 -3
  27. package/Components/ComponentResolver/index.ts +1 -1
  28. package/Components/FeedLoader/FeedLoader.tsx +7 -16
  29. package/Components/FeedLoader/FeedLoaderHOC.tsx +21 -0
  30. package/Components/FeedLoader/index.js +2 -8
  31. package/Components/Focusable/Focusable.tsx +12 -3
  32. package/Components/Focusable/FocusableTvOS.tsx +5 -5
  33. package/Components/Focusable/FocusableiOS.tsx +2 -2
  34. package/Components/Focusable/Touchable.tsx +5 -3
  35. package/Components/Focusable/__tests__/index.android.test.tsx +3 -0
  36. package/Components/Focusable/index.android.tsx +19 -11
  37. package/Components/Focusable/index.tsx +1 -1
  38. package/Components/FocusableGroup/FocusableTvOS.tsx +1 -1
  39. package/Components/FocusableList/FocusableItem.tsx +4 -3
  40. package/Components/FocusableList/FocusableListItemWrapper.tsx +2 -1
  41. package/Components/FocusableList/hooks/useCellState.android.ts +13 -3
  42. package/Components/FocusableList/index.tsx +20 -9
  43. package/Components/FreezeWithCallback/__tests__/index.test.tsx +67 -43
  44. package/Components/GeneralContentScreen/utils/__tests__/useCurationAPI.test.js +42 -59
  45. package/Components/GeneralContentScreen/utils/useCurationAPI.ts +13 -10
  46. package/Components/HandlePlayable/HandlePlayable.tsx +25 -9
  47. package/Components/HookRenderer/HookRenderer.tsx +5 -1
  48. package/Components/Layout/TV/LayoutBackground.tsx +1 -1
  49. package/Components/Layout/TV/__tests__/index.test.tsx +0 -1
  50. package/Components/MasterCell/DefaultComponents/ActionButton.tsx +6 -2
  51. package/Components/MasterCell/DefaultComponents/Button.tsx +1 -1
  52. package/Components/MasterCell/DefaultComponents/FocusableView/index.tsx +4 -39
  53. package/Components/MasterCell/DefaultComponents/Image/hoc/withDimensions.tsx +1 -1
  54. package/Components/MasterCell/DefaultComponents/ImageContainer/index.tsx +1 -1
  55. package/Components/MasterCell/DefaultComponents/SecondaryImage/Image.tsx +65 -17
  56. package/Components/MasterCell/DefaultComponents/SecondaryImage/__tests__/Image.test.tsx +21 -3
  57. package/Components/MasterCell/DefaultComponents/SecondaryImage/__tests__/__snapshots__/Image.test.tsx.snap +6 -3
  58. package/Components/MasterCell/DefaultComponents/Text/index.tsx +26 -6
  59. package/Components/MasterCell/DefaultComponents/__tests__/image.test.js +10 -10
  60. package/Components/MasterCell/DefaultComponents/__tests__/text.test.tsx +18 -18
  61. package/Components/MasterCell/SharedUI/CollapsibleTextContainer/__tests__/index.test.tsx +10 -10
  62. package/Components/MasterCell/elementMapper.tsx +1 -2
  63. package/Components/MasterCell/index.tsx +1 -1
  64. package/Components/MasterCell/utils/behaviorProvider.ts +82 -14
  65. package/Components/MasterCell/utils/index.ts +11 -5
  66. package/Components/OfflineHandler/NotificationView/__tests__/index.test.tsx +13 -18
  67. package/Components/OfflineHandler/__tests__/__snapshots__/index.test.tsx.snap +9 -0
  68. package/Components/OfflineHandler/__tests__/index.test.tsx +26 -35
  69. package/Components/PlayerContainer/ErrorDisplay/index.ts +1 -1
  70. package/Components/PlayerContainer/PlayerContainer.tsx +46 -33
  71. package/Components/PlayerContainer/ProgramInfo/index.tsx +1 -1
  72. package/Components/PlayerContainer/index.ts +1 -1
  73. package/Components/PlayerImageBackground/index.tsx +1 -1
  74. package/Components/River/ComponentsMap/ComponentsMap.tsx +0 -1
  75. package/Components/River/ComponentsMap/hooks/__tests__/useLoadingState.test.ts +378 -0
  76. package/Components/River/ComponentsMap/hooks/useLoadingState.ts +2 -2
  77. package/Components/River/RefreshControl.tsx +11 -17
  78. package/Components/River/RiverItem.tsx +3 -0
  79. package/Components/River/TV/River.tsx +2 -17
  80. package/Components/River/TV/index.tsx +3 -1
  81. package/Components/River/TV/withPipesV1DataLoader.tsx +43 -0
  82. package/Components/River/TV/withRiverDataLoader.tsx +17 -0
  83. package/Components/River/TV/withTVEventHandler.tsx +1 -1
  84. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +2 -0
  85. package/Components/River/__tests__/river.test.js +12 -26
  86. package/Components/River/index.tsx +1 -1
  87. package/Components/Screen/__tests__/Screen.test.tsx +28 -29
  88. package/Components/Screen/__tests__/navigationHandler.test.ts +133 -22
  89. package/Components/Screen/navigationHandler.ts +20 -2
  90. package/Components/ScreenResolver/index.tsx +15 -0
  91. package/Components/ScreenRevealManager/ScreenRevealManager.ts +76 -0
  92. package/Components/ScreenRevealManager/__tests__/ScreenRevealManager.test.ts +107 -0
  93. package/Components/ScreenRevealManager/__tests__/withScreenRevealManager.test.tsx +96 -0
  94. package/Components/ScreenRevealManager/index.ts +1 -0
  95. package/Components/ScreenRevealManager/withScreenRevealManager.tsx +79 -0
  96. package/Components/Tabs/TV/Tabs.android.tsx +1 -3
  97. package/Components/Tabs/Tabs.tsx +2 -3
  98. package/Components/TextInputTv/__tests__/__snapshots__/TextInputTv.test.js.snap +13 -0
  99. package/Components/TextInputTv/index.tsx +11 -0
  100. package/Components/Touchable/__tests__/__snapshots__/touchable.test.tsx.snap +34 -0
  101. package/Components/Touchable/__tests__/touchable.test.tsx +12 -17
  102. package/Components/Transitioner/__tests__/__snapshots__/Scene.test.js.snap +15 -9
  103. package/Components/VideoLive/animationUtils.ts +3 -3
  104. package/Components/VideoModal/ModalAnimation/AnimatedScrollModal.tsx +3 -9
  105. package/Components/VideoModal/ModalAnimation/AnimatedScrollModal.web.tsx +294 -0
  106. package/Components/VideoModal/ModalAnimation/AnimatedVideoPlayerComponent.web.tsx +93 -0
  107. package/Components/VideoModal/ModalAnimation/ModalAnimationContext.tsx +73 -29
  108. package/Components/VideoModal/PlayerDetails.tsx +24 -2
  109. package/Components/VideoModal/PlayerWrapper.tsx +26 -142
  110. package/Components/VideoModal/VideoModal.tsx +3 -17
  111. package/Components/VideoModal/__tests__/PlayerDetails.test.tsx +5 -5
  112. package/Components/VideoModal/__tests__/PlayerWrapper.test.tsx +1 -7
  113. package/Components/VideoModal/__tests__/__snapshots__/PlayerWrapper.test.tsx.snap +44 -240
  114. package/Components/VideoModal/hooks/__tests__/useDelayedPlayerDetails.test.ts +9 -1
  115. package/Components/VideoModal/hooks/index.ts +0 -2
  116. package/Components/VideoModal/hooks/useDelayedPlayerDetails.ts +40 -15
  117. package/Components/VideoModal/hooks/useModalSize.ts +18 -2
  118. package/Components/VideoModal/hooks/utils/__tests__/showDetails.test.ts +2 -2
  119. package/Components/VideoModal/hooks/utils/index.ts +4 -0
  120. package/Components/VideoModal/utils.ts +6 -0
  121. package/Components/Viewport/ViewportAware/__tests__/viewportAware.test.js +12 -16
  122. package/Components/Viewport/ViewportTracker/__tests__/viewportTracker.test.js +84 -24
  123. package/Components/Viewport/VisibilitySensor/VisibilitySensor.tsx +3 -3
  124. package/Components/default-cell-renderer/viewTrees/tv/DefaultCell/index.ts +3 -3
  125. package/Contexts/CellFocusedStateContext/index.tsx +27 -0
  126. package/Contexts/ConfigutaionContext/__tests__/ConfigurationProvider.test.tsx +3 -3
  127. package/Contexts/ScreenContext/index.tsx +46 -6
  128. package/Decorators/ConfigurationWrapper/__tests__/withConfigurationProvider.test.tsx +3 -3
  129. package/Decorators/ConfigurationWrapper/withConfigurationProvider.tsx +2 -2
  130. package/Decorators/RiverFeedLoader/__tests__/__snapshots__/riverFeedLoader.test.tsx.snap +221 -209
  131. package/Decorators/RiverFeedLoader/__tests__/riverFeedLoader.test.tsx +14 -16
  132. package/Decorators/RiverFeedLoader/__tests__/utils.test.ts +0 -20
  133. package/Decorators/RiverFeedLoader/index.tsx +22 -4
  134. package/Decorators/RiverFeedLoader/utils/index.ts +0 -18
  135. package/Decorators/RiverResolver/__tests__/riverResolver.test.tsx +3 -6
  136. package/Decorators/ZappPipesDataConnector/ResolverSelector.tsx +25 -0
  137. package/Decorators/ZappPipesDataConnector/__tests__/NullFeedResolver.test.tsx +78 -0
  138. package/Decorators/ZappPipesDataConnector/__tests__/ResolverSelector.test.tsx +205 -0
  139. package/Decorators/ZappPipesDataConnector/__tests__/StaticFeedResolver.test.tsx +251 -0
  140. package/Decorators/ZappPipesDataConnector/__tests__/UrlFeedResolver.test.tsx +368 -0
  141. package/Decorators/ZappPipesDataConnector/__tests__/utils.test.ts +39 -0
  142. package/Decorators/ZappPipesDataConnector/index.tsx +26 -293
  143. package/Decorators/ZappPipesDataConnector/resolvers/NullFeedResolver.tsx +25 -0
  144. package/Decorators/ZappPipesDataConnector/resolvers/StaticFeedResolver.tsx +87 -0
  145. package/Decorators/ZappPipesDataConnector/resolvers/UrlFeedResolver.tsx +266 -0
  146. package/Decorators/ZappPipesDataConnector/types.ts +29 -0
  147. package/Decorators/ZappPipesDataConnector/utils/mongoFilter.ts +738 -0
  148. package/Decorators/ZappPipesDataConnector/utils/useFilter.tsx +157 -0
  149. package/events/index.ts +3 -0
  150. package/package.json +5 -10
  151. package/Components/AudioPlayer/AudioPlayerLayout.tsx +0 -202
  152. package/Components/AudioPlayer/__tests__/__snapshots__/audioPlayerLayout.test.js.snap +0 -66
  153. package/Components/AudioPlayer/__tests__/__snapshots__/channel.test.js.snap +0 -28
  154. package/Components/AudioPlayer/__tests__/audioPlayerLayout.test.js +0 -26
  155. package/Components/AudioPlayer/index.ts +0 -1
  156. package/Components/River/__tests__/__snapshots__/river.test.js.snap +0 -27
  157. package/Components/VideoModal/hooks/useBackgroundColor.ts +0 -10
  158. /package/Components/AudioPlayer/{__tests__ → tv/__tests__}/Runtime.test.js +0 -0
  159. /package/Components/AudioPlayer/{__tests__ → tv/__tests__}/__snapshots__/artWork.test.js.snap +0 -0
  160. /package/Components/AudioPlayer/{__tests__ → tv/__tests__}/artWork.test.js +0 -0
  161. /package/Components/AudioPlayer/{__tests__ → tv/__tests__}/channel.test.js +0 -0
  162. /package/Components/AudioPlayer/{__tests__ → tv/__tests__}/summary.test.js +0 -0
  163. /package/Components/AudioPlayer/{__tests__ → tv/__tests__}/title.test.js +0 -0
@@ -0,0 +1,738 @@
1
+ import { createLogger } from "@applicaster/zapp-react-native-utils/logger";
2
+
3
+ const { log_error } = createLogger({
4
+ subsystem: "General",
5
+ category: "MongoSyntaxFilter",
6
+ });
7
+
8
+ // types.ts (or at the top of your file)
9
+ type JsonPrimitive = string | number | boolean | null;
10
+ type JsonValue = JsonPrimitive | JsonObject | JsonArray;
11
+ interface JsonObject {
12
+ [key: string]: JsonValue;
13
+ }
14
+ interface JsonArray extends Array<JsonValue> {}
15
+
16
+ type MongoQuery = {
17
+ [key: string]: any; // Values can be direct matches or operator objects
18
+ };
19
+
20
+ // --- Helper Functions ---
21
+
22
+ /**
23
+ * Retrieves a value from an object using a dot-notation path.
24
+ * @param obj The object to traverse.
25
+ * @param path The dot-notation path (e.g., "a.b.c").
26
+ * @returns The value at the specified path, or undefined if the path doesn't exist.
27
+ */
28
+ function getValueByPath(obj: JsonObject, path: string): any {
29
+ if (path === "" || path === undefined) return undefined;
30
+ const keys = path.split(".");
31
+ let current: any = obj;
32
+
33
+ for (const key of keys) {
34
+ if (
35
+ current &&
36
+ typeof current === "object" &&
37
+ Object.prototype.hasOwnProperty.call(current, key)
38
+ ) {
39
+ current = (current as JsonObject)[key];
40
+ } else {
41
+ return undefined; // Path does not fully resolve or encounters non-object/missing key
42
+ }
43
+ }
44
+
45
+ return current;
46
+ }
47
+
48
+ /**
49
+ * Checks if a dot-notation path exists within an object.
50
+ * @param obj The object to check.
51
+ * @param path The dot-notation path.
52
+ * @returns True if the path exists, false otherwise.
53
+ */
54
+ function pathExists(obj: JsonObject, path: string): boolean {
55
+ if (path === "" || path === undefined) return false;
56
+ const keys = path.split(".");
57
+ let current: any = obj;
58
+
59
+ for (let i = 0; i < keys.length; i++) {
60
+ const key = keys[i];
61
+
62
+ if (
63
+ typeof current === "object" &&
64
+ current !== null &&
65
+ Object.prototype.hasOwnProperty.call(current, key)
66
+ ) {
67
+ if (i === keys.length - 1) {
68
+ return true; // Full path exists
69
+ }
70
+
71
+ current = (current as JsonObject)[key];
72
+ } else {
73
+ return false; // Path does not exist
74
+ }
75
+ }
76
+
77
+ return false; // Should be covered by loop logic
78
+ }
79
+
80
+ /**
81
+ * Performs a basic deep equality check between two values.
82
+ * Handles primitives, arrays, and objects.
83
+ * @param val1 First value.
84
+ * @param val2 Second value.
85
+ * @returns True if values are deeply equal, false otherwise.
86
+ */
87
+ function isEqual(val1: any, val2: any): boolean {
88
+ if (val1 === val2) return true; // Handles primitives and same object/array references
89
+
90
+ if (typeof val1 !== typeof val2) return false;
91
+ if (val1 === null || val2 === null) return false; // Already covered by val1 === val2 if both are null
92
+
93
+ if (typeof val1 === "object") {
94
+ if (Array.isArray(val1)) {
95
+ if (!Array.isArray(val2) || val1.length !== val2.length) return false;
96
+
97
+ for (let i = 0; i < val1.length; i++) {
98
+ if (!isEqual(val1[i], val2[i])) return false;
99
+ }
100
+
101
+ return true;
102
+ } else {
103
+ // Both are objects
104
+ if (Array.isArray(val2)) return false;
105
+ const keys1 = Object.keys(val1);
106
+ const keys2 = Object.keys(val2);
107
+ if (keys1.length !== keys2.length) return false;
108
+
109
+ for (const key of keys1) {
110
+ if (
111
+ !Object.prototype.hasOwnProperty.call(val2, key) ||
112
+ !isEqual(val1[key], val2[key])
113
+ ) {
114
+ return false;
115
+ }
116
+ }
117
+
118
+ return true;
119
+ }
120
+ }
121
+
122
+ return false; // Mismatch for other types or conditions
123
+ }
124
+
125
+ /**
126
+ * Evaluates if a value matches a specific condition object (e.g., { $gt: 10, $lt: 20 }).
127
+ * This is used by the $not operator.
128
+ * @param value The actual value from the document field.
129
+ * @param conditionObject The condition object (e.g., { $gt: 10 }).
130
+ * @returns True if the value satisfies all conditions in conditionObject.
131
+ */
132
+ function checkValueAgainstOperatorObject(
133
+ value: any,
134
+ conditionObject: JsonObject
135
+ ): boolean {
136
+ for (const operator in conditionObject) {
137
+ if (Object.prototype.hasOwnProperty.call(conditionObject, operator)) {
138
+ const operand = conditionObject[operator];
139
+
140
+ // This is a simplified version of evaluateFieldCondition's operator switch,
141
+ // applied directly to 'value'. $exists is not applicable here.
142
+ switch (operator) {
143
+ case "$eq":
144
+ if (!isEqual(value, operand)) return false;
145
+ break;
146
+ case "$ne":
147
+ if (isEqual(value, operand)) return false;
148
+ break;
149
+ case "$gt":
150
+ if (value === undefined || value === null || !(value > operand)) {
151
+ return false;
152
+ }
153
+
154
+ break;
155
+ case "$gte":
156
+ if (value === undefined || value === null || !(value >= operand)) {
157
+ return false;
158
+ }
159
+
160
+ break;
161
+ case "$lt":
162
+ if (value === undefined || value === null || !(value < operand)) {
163
+ return false;
164
+ }
165
+
166
+ break;
167
+ case "$lte":
168
+ if (value === undefined || value === null || !(value <= operand)) {
169
+ return false;
170
+ }
171
+
172
+ break;
173
+ case "$in":
174
+ if (!Array.isArray(operand)) {
175
+ throw new Error("$in requires an array operand.");
176
+ }
177
+
178
+ if (Array.isArray(value)) {
179
+ // If field value is an array
180
+ if (!value.some((item) => operand.includes(item))) return false;
181
+ } else {
182
+ if (!operand.includes(value)) return false;
183
+ }
184
+
185
+ break;
186
+ case "$nin":
187
+ if (!Array.isArray(operand)) {
188
+ throw new Error("$nin requires an array operand.");
189
+ }
190
+
191
+ if (Array.isArray(value)) {
192
+ // If field value is an array
193
+ if (value.some((item) => operand.includes(item))) return false;
194
+ } else {
195
+ if (operand.includes(value)) return false;
196
+ }
197
+
198
+ break;
199
+ // Regex and other relevant operators can be added here if needed for $not's operand
200
+ default:
201
+ log_error(
202
+ `Unsupported operator '${operator}' inside $not's operand object.`
203
+ );
204
+
205
+ return false;
206
+ }
207
+ }
208
+ }
209
+
210
+ return true; // All conditions in conditionObject passed for 'value'
211
+ }
212
+
213
+ /**
214
+ * Checks if a given object matches a MongoDB-style query.
215
+ * @param obj The object to check.
216
+ * @param query The MongoDB-style query.
217
+ * @returns True if the object matches the query, false otherwise.
218
+ */
219
+ function matchesQuery(obj: JsonObject, query: MongoQuery): boolean {
220
+ for (const key in query) {
221
+ if (Object.prototype.hasOwnProperty.call(query, key)) {
222
+ const condition = query[key];
223
+
224
+ if (key.startsWith("$")) {
225
+ // Top-level logical operators ($and, $or, $not, $nor)
226
+ switch (key) {
227
+ case "$and":
228
+ if (!Array.isArray(condition)) {
229
+ throw new Error(
230
+ "$and operator requires an array of query objects."
231
+ );
232
+ }
233
+
234
+ if (
235
+ !condition.every((subQuery: MongoQuery) =>
236
+ matchesQuery(obj, subQuery)
237
+ )
238
+ ) {
239
+ return false;
240
+ }
241
+
242
+ break;
243
+ case "$or":
244
+ if (!Array.isArray(condition)) {
245
+ throw new Error(
246
+ "$or operator requires an array of query objects."
247
+ );
248
+ }
249
+
250
+ if (
251
+ !condition.some((subQuery: MongoQuery) =>
252
+ matchesQuery(obj, subQuery)
253
+ )
254
+ ) {
255
+ return false;
256
+ }
257
+
258
+ break;
259
+ case "$not": // Top-level $not
260
+ if (typeof condition !== "object" || condition === null) {
261
+ throw new Error("Top-level $not requires a query object.");
262
+ }
263
+
264
+ if (matchesQuery(obj, condition as MongoQuery)) {
265
+ return false; // If inner query matches, $not fails
266
+ }
267
+
268
+ break;
269
+ case "$nor":
270
+ if (!Array.isArray(condition)) {
271
+ throw new Error(
272
+ "$nor operator requires an array of query objects."
273
+ );
274
+ }
275
+
276
+ if (
277
+ condition.some((subQuery: MongoQuery) =>
278
+ matchesQuery(obj, subQuery)
279
+ )
280
+ ) {
281
+ return false; // If any subQuery matches, $nor fails
282
+ }
283
+
284
+ break;
285
+ default:
286
+ log_error(`Unsupported top-level logical operator: ${key}`);
287
+
288
+ return false;
289
+ }
290
+ } else {
291
+ // Field-specific query
292
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
293
+ if (!evaluateFieldCondition(obj, key, condition)) {
294
+ return false;
295
+ }
296
+ }
297
+ }
298
+ }
299
+
300
+ return true; // All conditions in the query passed
301
+ }
302
+
303
+ /**
304
+ * Evaluates if a document's field satisfies a given condition.
305
+ * @param obj The document being checked.
306
+ * @param fieldPath The path to the field in the document (e.g., "age" or "address.city").
307
+ * @param fieldCondition The condition for the field (e.g., 30, { $gte: 30 }, /pattern/).
308
+ * @returns True if the field satisfies the condition, false otherwise.
309
+ */
310
+ function evaluateFieldCondition(
311
+ obj: JsonObject,
312
+ fieldPath: string,
313
+ fieldCondition: any
314
+ ): boolean {
315
+ const actualValue = getValueByPath(obj, fieldPath);
316
+ const fieldActuallyExists = pathExists(obj, fieldPath);
317
+
318
+ // Case 1: fieldCondition is an operator object (e.g., { $gt: 10, $lt: 20 })
319
+ if (
320
+ typeof fieldCondition === "object" &&
321
+ fieldCondition !== null &&
322
+ !Array.isArray(fieldCondition) &&
323
+ !(fieldCondition instanceof RegExp)
324
+ ) {
325
+ const operatorKeys = Object.keys(fieldCondition).filter((k) =>
326
+ k.startsWith("$")
327
+ );
328
+
329
+ if (operatorKeys.length > 0) {
330
+ for (const operator in fieldCondition) {
331
+ if (!Object.prototype.hasOwnProperty.call(fieldCondition, operator)) {
332
+ continue;
333
+ }
334
+
335
+ if (
336
+ operator === "$options" &&
337
+ Object.prototype.hasOwnProperty.call(fieldCondition, "$regex")
338
+ ) {
339
+ continue;
340
+ } // Handled by $regex
341
+
342
+ const operand = fieldCondition[operator];
343
+
344
+ switch (operator) {
345
+ case "$eq":
346
+ if (operand === null) {
347
+ // MongoDB's special null equality
348
+ if (actualValue !== null && fieldActuallyExists) return false; // Matches if value is null or field doesn't exist
349
+ } else {
350
+ if (!isEqual(actualValue, operand)) return false;
351
+ }
352
+
353
+ break;
354
+ case "$ne":
355
+ // For $ne, null is treated literally. { field: { $ne: null } } means field must exist and not be null.
356
+ if (operand === null) {
357
+ if (actualValue === null || !fieldActuallyExists) return false;
358
+ } else {
359
+ if (isEqual(actualValue, operand)) return false;
360
+ }
361
+
362
+ break;
363
+ case "$gt":
364
+ if (
365
+ actualValue === undefined ||
366
+ actualValue === null ||
367
+ !(actualValue > operand)
368
+ ) {
369
+ return false;
370
+ }
371
+
372
+ break;
373
+ case "$gte":
374
+ if (
375
+ actualValue === undefined ||
376
+ actualValue === null ||
377
+ !(actualValue >= operand)
378
+ ) {
379
+ return false;
380
+ }
381
+
382
+ break;
383
+ case "$lt":
384
+ if (
385
+ actualValue === undefined ||
386
+ actualValue === null ||
387
+ !(actualValue < operand)
388
+ ) {
389
+ return false;
390
+ }
391
+
392
+ break;
393
+ case "$lte":
394
+ if (
395
+ actualValue === undefined ||
396
+ actualValue === null ||
397
+ !(actualValue <= operand)
398
+ ) {
399
+ return false;
400
+ }
401
+
402
+ break;
403
+ case "$in":
404
+ if (!Array.isArray(operand)) {
405
+ throw new Error("$in operator requires an array operand.");
406
+ }
407
+
408
+ if (!fieldActuallyExists) {
409
+ // Field does not exist
410
+ if (!operand.includes(null)) return false; // Matches if null is in operand array
411
+ } else if (Array.isArray(actualValue)) {
412
+ // Field is an array
413
+ if (!actualValue.some((item) => operand.includes(item))) {
414
+ return false;
415
+ }
416
+ } else {
417
+ // Field is a single value
418
+ if (!operand.includes(actualValue)) return false;
419
+ }
420
+
421
+ break;
422
+ case "$nin":
423
+ if (!Array.isArray(operand)) {
424
+ throw new Error("$nin operator requires an array operand.");
425
+ }
426
+
427
+ if (!fieldActuallyExists) {
428
+ // Field does not exist
429
+ if (operand.includes(null)) return false; // Does NOT match if null is in operand array
430
+ } else if (Array.isArray(actualValue)) {
431
+ // Field is an array
432
+ if (actualValue.some((item) => operand.includes(item))) {
433
+ return false;
434
+ }
435
+ } else {
436
+ // Field is a single value
437
+ if (operand.includes(actualValue)) return false;
438
+ }
439
+
440
+ break;
441
+ case "$exists":
442
+ if (typeof operand !== "boolean") {
443
+ throw new Error("$exists operator requires a boolean operand.");
444
+ }
445
+
446
+ if (operand === true && !fieldActuallyExists) return false;
447
+ if (operand === false && fieldActuallyExists) return false;
448
+ break;
449
+
450
+ case "$type": {
451
+ if (typeof operand !== "string") {
452
+ throw new Error("$type requires a string operand.");
453
+ }
454
+
455
+ const jsType = typeof actualValue;
456
+
457
+ let matchesType = false;
458
+
459
+ if (
460
+ !fieldActuallyExists &&
461
+ operand !==
462
+ "null" /* and other types that could match non-existence if we were super strict */
463
+ ) {
464
+ // Generally, $type checks existing fields. If field doesn't exist, it won't match most types.
465
+ // Exception: $type: "null" could match if we consider missing field as null for $type.
466
+ // MongoDB: $type matches BSON types. For non-existent fields, it depends.
467
+ // For simplicity, if field doesn't exist, it only matches $type: "null" if actualValue is undefined and operand is "null" (which is not how it works)
468
+ // Let's be strict: if field doesn't exist, it doesn't have a type other than "undefined" (which isn't a Mongo type string).
469
+ // If operand is 'null', it checks if actualValue is null. If field is missing, actualValue is undefined.
470
+ if (operand === "null") {
471
+ // $type: 'null'
472
+ matchesType = actualValue === null; // Only if value is literally null
473
+ } else {
474
+ matchesType = false;
475
+ }
476
+ } else {
477
+ switch (operand) {
478
+ case "string":
479
+ matchesType = jsType === "string";
480
+ break;
481
+ case "number":
482
+ matchesType =
483
+ jsType === "number" &&
484
+ Number.isFinite(actualValue as number);
485
+
486
+ break;
487
+ case "boolean":
488
+ matchesType = jsType === "boolean";
489
+ break;
490
+ case "object":
491
+ matchesType =
492
+ jsType === "object" &&
493
+ actualValue !== null &&
494
+ !Array.isArray(actualValue);
495
+
496
+ break;
497
+ case "array":
498
+ matchesType = Array.isArray(actualValue);
499
+ break;
500
+ case "null":
501
+ matchesType = actualValue === null;
502
+ break;
503
+ default:
504
+ throw new Error(
505
+ `Unsupported type string for $type: ${operand}`
506
+ );
507
+ }
508
+ }
509
+
510
+ if (!matchesType) return false;
511
+ break;
512
+ }
513
+
514
+ case "$all": {
515
+ if (!Array.isArray(operand)) {
516
+ throw new Error("$all requires an array operand.");
517
+ }
518
+
519
+ if (!fieldActuallyExists || !Array.isArray(actualValue)) {
520
+ return false;
521
+ } // Field must exist and be an array
522
+
523
+ // Assumes operand contains literal values for $all
524
+ const allPresent = operand.every((itemInOperand) =>
525
+ actualValue.some((valInArray) =>
526
+ isEqual(valInArray, itemInOperand)
527
+ )
528
+ );
529
+
530
+ if (!allPresent) return false;
531
+ break;
532
+ }
533
+
534
+ case "$elemMatch": {
535
+ if (typeof operand !== "object" || operand === null) {
536
+ throw new Error("$elemMatch requires a query object.");
537
+ }
538
+
539
+ if (!fieldActuallyExists || !Array.isArray(actualValue)) {
540
+ return false;
541
+ } // Field must exist and be an array
542
+
543
+ const foundElemMatch = actualValue.some((element) => {
544
+ if (typeof element !== "object" || element === null) return false; // Elements must be objects for sub-query
545
+
546
+ return matchesQuery(element as JsonObject, operand as MongoQuery);
547
+ });
548
+
549
+ if (!foundElemMatch) return false;
550
+ break;
551
+ }
552
+
553
+ case "$size":
554
+ if (
555
+ typeof operand !== "number" ||
556
+ !Number.isInteger(operand) ||
557
+ operand < 0
558
+ ) {
559
+ throw new Error("$size requires a non-negative integer.");
560
+ }
561
+
562
+ if (!fieldActuallyExists || !Array.isArray(actualValue)) {
563
+ return false;
564
+ } // Field must exist and be an array
565
+
566
+ if (actualValue.length !== operand) return false;
567
+ break;
568
+
569
+ case "$regex": {
570
+ const pattern = operand;
571
+ const options = fieldCondition.$options || "";
572
+ let regexInstance: RegExp;
573
+
574
+ if (pattern instanceof RegExp) {
575
+ regexInstance = new RegExp(
576
+ pattern.source,
577
+ options || pattern.flags
578
+ );
579
+ } else if (typeof pattern === "string") {
580
+ regexInstance = new RegExp(pattern, options);
581
+ } else {
582
+ throw new Error(
583
+ "$regex pattern must be a string or RegExp instance."
584
+ );
585
+ }
586
+
587
+ if (
588
+ typeof actualValue !== "string" ||
589
+ !regexInstance.test(actualValue)
590
+ ) {
591
+ return false;
592
+ }
593
+
594
+ break;
595
+ }
596
+
597
+ case "$not":
598
+ // Operand for $not is an operator expression object (e.g., { $gt: 10 }) or a RegExp
599
+ if (operand instanceof RegExp) {
600
+ if (
601
+ typeof actualValue === "string" &&
602
+ operand.test(actualValue)
603
+ ) {
604
+ return false;
605
+ } // If regex matches, $not fails
606
+ } else if (typeof operand === "object" && operand !== null) {
607
+ if (
608
+ checkValueAgainstOperatorObject(
609
+ actualValue,
610
+ operand as JsonObject
611
+ )
612
+ ) {
613
+ return false;
614
+ } // If inner condition matches, $not fails
615
+ } else {
616
+ throw new Error(
617
+ "$not requires an operator expression or RegExp."
618
+ );
619
+ }
620
+
621
+ break;
622
+ default:
623
+ log_error(`Unsupported field operator: ${operator}`);
624
+
625
+ return false;
626
+ }
627
+ }
628
+
629
+ return true; // All operator conditions for this field passed
630
+ } else {
631
+ // Case 2: fieldCondition is an object for sub-document matching (e.g., { subField: "value" })
632
+ if (typeof actualValue !== "object" || actualValue === null) return false; // actualValue must be an object for sub-query
633
+
634
+ return matchesQuery(
635
+ actualValue as JsonObject,
636
+ fieldCondition as MongoQuery
637
+ );
638
+ }
639
+ } else {
640
+ // Case 3: fieldCondition is a primitive, RegExp, or null (implicit $eq)
641
+ if (fieldCondition instanceof RegExp) {
642
+ return (
643
+ typeof actualValue === "string" && fieldCondition.test(actualValue)
644
+ );
645
+ }
646
+
647
+ // MongoDB's special null equality for implicit { field: null }
648
+ if (fieldCondition === null) {
649
+ return actualValue === null || !fieldActuallyExists; // Matches if value is null or field doesn't exist
650
+ }
651
+
652
+ return isEqual(actualValue, fieldCondition);
653
+ }
654
+ }
655
+
656
+ // --- Main Exported Function ---
657
+
658
+ /**
659
+ * Filters an array of JSON objects based on a MongoDB-style query filter.
660
+ * @param data Array of JSON objects to filter.
661
+ * @param query The MongoDB-style query filter object.
662
+ * @returns A new array containing only the objects that match the query.
663
+ *
664
+ * @example
665
+ * const data = [
666
+ * { name: "Alice", age: 30, city: "New York", tags: ["dev", "js"] },
667
+ * { name: "Bob", age: 24, city: "London", tags: ["dev", "python"] },
668
+ * { name: "Charlie", age: 30, city: "Paris", tags: ["qa", "js"] },
669
+ * { name: "David", age: null, city: "Berlin" }
670
+ * ];
671
+ *
672
+ * // Find users older than 25
673
+ * filterObjects(data, { age: { $gt: 25 } });
674
+ * // Result: Alice, Charlie
675
+ *
676
+ * // Find users in New York or London
677
+ * filterObjects(data, { $or: [{ city: "New York" }, { city: "London" }] });
678
+ * // Result: Alice, Bob
679
+ *
680
+ * // Find users with the "dev" tag
681
+ * filterObjects(data, { tags: "dev" }); // or { tags: { $in: ["dev"] } } or { tags: { $all: ["dev"] } }
682
+ * // Result: Alice, Bob
683
+ *
684
+ * // Find users where age is null or missing
685
+ * filterObjects(data, { age: null });
686
+ * // Result: David
687
+ */
688
+ export function filterObjects(
689
+ data: JsonObject[],
690
+ query: MongoQuery
691
+ ): JsonObject[] {
692
+ if (!data || data.length === 0) return [];
693
+ if (Object.keys(query).length === 0) return [...data]; // Return all if query is empty
694
+
695
+ return data.filter((obj) => matchesQuery(obj, query));
696
+ }
697
+
698
+ // Example Usage (can be removed or commented out in final library code)
699
+ /*
700
+ const sampleData: JsonObject[] = [
701
+ { _id: 1, item: { name: "ab", code: "123" }, qty: 15, tags: ["A", "B", "C"], scores: [ { s: 10, c:5 }, {s: 7, c: 8} ], status: "Active" },
702
+ { _id: 2, item: { name: "cd", code: "123" }, qty: 20, tags: ["B"], scores: [ { s: 8, c:5 } ], status: "Inactive" },
703
+ { _id: 3, item: { name: "ij", code: "456" }, qty: 25, tags: ["A", "B"], scores: [], status: "Active", meta: null },
704
+ { _id: 4, item: { name: "xy", code: "456" }, qty: 30, tags: ["B", "C"], status: "Pending" },
705
+ { _id: 5, item: { name: "mn", code: "000" }, qty: 20, tags: [["A", "B"], "C"] , meta: { info: "details" }, size: { w: 10, h: 5, uom: "cm"} },
706
+ { _id: 6, item: { name: "ab", code: "123" }, qty: 10, tags: ["A"], description: "A blue pen." },
707
+ { _id: 7, item: { name: "zz", code: "789" }, qty: null, tags: ["D"] }, // qty is null
708
+ { _id: 8, item: { name: "yy", code: "000" } , tags: ["E"], status: "Active"} // qty is missing
709
+ ];
710
+
711
+ console.log("--- Test Cases ---");
712
+
713
+ console.log("1. Equality (qty: 15):", filterObjects(sampleData, { qty: 15 }));
714
+ console.log("2. Comparison (qty > 20):", filterObjects(sampleData, { qty: { $gt: 20 } }));
715
+ console.log("3. $in (tags: 'A' or 'D'):", filterObjects(sampleData, { tags: { $in: ["A", "D"] } }));
716
+ console.log("4. $all (tags: 'A' and 'B'):", filterObjects(sampleData, { tags: { $all: ["A", "B"] } }));
717
+ console.log("5. $exists (description exists):", filterObjects(sampleData, { description: { $exists: true } }));
718
+ console.log("6. $exists (description does not exist):", filterObjects(sampleData, { description: { $exists: false } }));
719
+ console.log("7. Nested field (item.name: 'ab'):", filterObjects(sampleData, { "item.name": "ab" }));
720
+ console.log("8. $and (status: 'Active', qty > 10):", filterObjects(sampleData, { $and: [{ status: "Active" }, { qty: { $gt: 10 } }] }));
721
+ console.log("9. $or (qty < 15 or item.code: '456'):", filterObjects(sampleData, { $or: [{ qty: { $lt: 15 } }, { "item.code": "456" }] }));
722
+ console.log("10. Regex (description contains 'pen'):", filterObjects(sampleData, { description: /pen/i }));
723
+ console.log("11. $regex (description starts with 'A'):", filterObjects(sampleData, { description: { $regex: "^A", $options: "i" } }));
724
+ console.log("12. Sub-document match (item: { name: 'ab', code: '123' }):", filterObjects(sampleData, { item: { name: "ab", code: "123" } }));
725
+ console.log("13. $elemMatch (scores has s >= 8):", filterObjects(sampleData, { scores: { $elemMatch: { s: { $gte: 8 } } } }));
726
+ console.log("14. $size (tags array size 1):", filterObjects(sampleData, { tags: { $size: 1 } }));
727
+ console.log("15. $not field level (qty not > 20):", filterObjects(sampleData, { qty: { $not: { $gt: 20 } } }));
728
+ console.log("16. Top-level $not (not status Active):", filterObjects(sampleData, { $not: { status: "Active" } }));
729
+ console.log("17. $type (qty is number):", filterObjects(sampleData, { qty: { $type: "number" } }));
730
+ console.log("18. $type (meta is object):", filterObjects(sampleData, { meta: { $type: "object" } }));
731
+ console.log("19. $type (tags is array):", filterObjects(sampleData, { tags: { $type: "array" } }));
732
+ console.log("20. Query for null value (qty: null - matches null or missing):", filterObjects(sampleData, { qty: null }));
733
+ console.log("21. Query for $ne null (qty: { $ne: null } - matches existing and not null):", filterObjects(sampleData, { qty: { $ne: null } }));
734
+ console.log("22. Query for $in including null (qty: { $in: [15, null] }):", filterObjects(sampleData, { qty: { $in: [15, null] } }));
735
+ console.log("23. Query for $nin including null (qty: { $nin: [20, null] }):", filterObjects(sampleData, { qty: { $nin: [20, null] } }));
736
+ console.log("24. Empty query:", filterObjects(sampleData, {}));
737
+ console.log("25. $nor (neither status Active nor qty < 20):", filterObjects(sampleData, { $nor: [{status: "Active"}, {qty: {$lt: 20}}]}));
738
+ */