@@ -18,120 +18,139 @@ package ai.tock.bot.mongo
1818
1919import org.bson.Document
2020import org.bson.conversions.Bson
21- import java.time.Instant
2221import java.time.ZonedDateTime
2322
2423/* *
2524 * Lightweight DSL for building MongoDB aggregation expressions.
26- *
25+ *
2726 * This DSL provides simple functions to construct MongoDB aggregation operators
2827 * in a more readable way than using raw strings, while producing identical BSON output.
29- *
28+ *
3029 * **Type Design**: Parameters are intentionally typed `Any` to support:
3130 * - BSON expressions (Document, Bson)
3231 * - MongoDB field paths ("$field")
3332 * - Aggregation variables ("$$value", "$$this")
3433 * - Primitive values (String, Int, null, etc.)
35- *
34+ *
3635 * Usage example:
3736 * ```
38- * val minDate = Agg .min(Agg .reduce(
39- * input = Agg .field("stories"),
37+ * val minDate = MongoAgg .min(MongoAgg .reduce(
38+ * input = MongoAgg .field("stories"),
4039 * initialValue = null,
41- * `in` = Agg .cond(
42- * Agg .eq(Agg .value(), null),
43- * Agg .ifNull(Agg .min("${Agg .thisVar()}.actions.date"), null),
44- * Agg .min(listOf(Agg .value(), Agg .ifNull(Agg .min("${Agg .thisVar()}.actions.date"), Agg .value())))
40+ * `in` = MongoAgg .cond(
41+ * MongoAgg .eq(MongoAgg .value(), null),
42+ * MongoAgg .ifNull(MongoAgg .min("${MongoAgg .thisVar()}.actions.date"), null),
43+ * MongoAgg .min(listOf(MongoAgg .value(), MongoAgg .ifNull(MongoAgg .min("${MongoAgg .thisVar()}.actions.date"), MongoAgg .value())))
4544 * )
4645 * ))
4746 * ```
4847 */
49- object Agg {
48+ object MongoAgg {
5049 /* *
5150 * Creates a $min aggregation expression.
52- *
51+ *
5352 * @param expr the expression to find the minimum of (can be null)
5453 * @return Document representing $min operator
5554 */
5655 fun min (expr : Any? ): Document = Document (" \$ min" , expr)
5756
5857 /* *
5958 * Creates a $max aggregation expression.
60- *
59+ *
6160 * @param expr the expression to find the maximum of (can be null)
6261 * @return Document representing $max operator
6362 */
6463 fun max (expr : Any? ): Document = Document (" \$ max" , expr)
6564
6665 /* *
6766 * Creates an $eq (equals) aggregation expression.
68- *
67+ *
6968 * @param a first operand
7069 * @param b second operand (can be null)
7170 * @return Document representing $eq operator
7271 */
73- fun eq (a : Any , b : Any? ): Document = Document (" \$ eq" , listOf (a, b))
72+ fun eq (
73+ a : Any ,
74+ b : Any? ,
75+ ): Document = Document (" \$ eq" , listOf (a, b))
7476
7577 /* *
7678 * Creates a $gte (greater than or equal) aggregation expression.
77- *
79+ *
7880 * @param a first operand
7981 * @param b second operand (can be null)
8082 * @return Document representing $gte operator
8183 */
82- fun gte (a : Any , b : Any? ): Document = Document (" \$ gte" , listOf (a, b))
84+ fun gte (
85+ a : Any ,
86+ b : Any? ,
87+ ): Document = Document (" \$ gte" , listOf (a, b))
8388
8489 /* *
8590 * Creates a $lte (less than or equal) aggregation expression.
86- *
91+ *
8792 * @param a first operand
8893 * @param b second operand (can be null)
8994 * @return Document representing $lte operator
9095 */
91- fun lte (a : Any , b : Any? ): Document = Document (" \$ lte" , listOf (a, b))
96+ fun lte (
97+ a : Any ,
98+ b : Any? ,
99+ ): Document = Document (" \$ lte" , listOf (a, b))
92100
93101 /* *
94102 * Creates a $gt (greater than) aggregation expression.
95- *
103+ *
96104 * @param a first operand
97105 * @param b second operand (can be null)
98106 * @return Document representing $gt operator
99107 */
100- fun gt (a : Any , b : Any? ): Document = Document (" \$ gt" , listOf (a, b))
108+ fun gt (
109+ a : Any ,
110+ b : Any? ,
111+ ): Document = Document (" \$ gt" , listOf (a, b))
101112
102113 /* *
103114 * Creates a $lt (less than) aggregation expression.
104- *
115+ *
105116 * @param a first operand
106117 * @param b second operand (can be null)
107118 * @return Document representing $lt operator
108119 */
109- fun lt (a : Any , b : Any? ): Document = Document (" \$ lt" , listOf (a, b))
120+ fun lt (
121+ a : Any ,
122+ b : Any? ,
123+ ): Document = Document (" \$ lt" , listOf (a, b))
110124
111125 /* *
112126 * Creates a $cond (conditional) aggregation expression.
113- *
127+ *
114128 * @param ifExpr the condition expression
115129 * @param thenExpr the expression to evaluate if condition is true (can be null)
116130 * @param elseExpr the expression to evaluate if condition is false (can be null)
117131 * @return Document representing $cond operator
118132 */
119- fun cond (ifExpr : Any , thenExpr : Any? , elseExpr : Any? ): Document =
120- Document (" \$ cond" , listOf (ifExpr, thenExpr, elseExpr))
133+ fun cond (
134+ ifExpr : Any ,
135+ thenExpr : Any? ,
136+ elseExpr : Any? ,
137+ ): Document = Document (" \$ cond" , listOf (ifExpr, thenExpr, elseExpr))
121138
122139 /* *
123140 * Creates an $ifNull aggregation expression.
124- *
141+ *
125142 * @param expr the expression to evaluate
126143 * @param replacement the replacement value if expr is null (can be null)
127144 * @return Document representing $ifNull operator
128145 */
129- fun ifNull (expr : Any , replacement : Any? ): Document =
130- Document (" \$ ifNull" , listOf (expr, replacement))
146+ fun ifNull (
147+ expr : Any ,
148+ replacement : Any? ,
149+ ): Document = Document (" \$ ifNull" , listOf (expr, replacement))
131150
132151 /* *
133152 * Creates a $reduce aggregation expression.
134- *
153+ *
135154 * @param input the array to reduce
136155 * @param initialValue the initial value for the accumulator (often null)
137156 * @param `in` the expression to apply to each element
@@ -140,24 +159,26 @@ object Agg {
140159 fun reduce (
141160 input : Any ,
142161 initialValue : Any? ,
143- `in `: Any
144- ): Document = Document (" \$ reduce" ,
145- Document (" input" , input)
146- .append(" initialValue" , initialValue)
147- .append(" in" , `in `)
148- )
162+ `in `: Any ,
163+ ): Document =
164+ Document (
165+ " \$ reduce" ,
166+ Document (" input" , input)
167+ .append(" initialValue" , initialValue)
168+ .append(" in" , `in `),
169+ )
149170
150171 /* *
151172 * Creates an $expr aggregation expression for use in queries.
152- *
173+ *
153174 * @param expr the expression to evaluate (can be Document, Bson, or any BSON-compatible value, can be null)
154175 * @return Bson representing $expr operator
155176 */
156177 fun expr (expr : Any? ): Bson = Document (" \$ expr" , expr)
157178
158179 /* *
159180 * Creates a MongoDB field path reference.
160- *
181+ *
161182 * @param path the field path (e.g., "stories" becomes "$stories")
162183 * @return String representing the MongoDB field path
163184 */
@@ -180,15 +201,15 @@ object Agg {
180201
181202 /* *
182203 * Creates a $and aggregation expression.
183- *
204+ *
184205 * @param exprs the expressions to combine with AND logic
185206 * @return Document representing $and operator
186207 */
187208 fun and (vararg exprs : Any ): Document = Document (" \$ and" , exprs.toList())
188209
189210 /* *
190211 * Creates an $or aggregation expression.
191- *
212+ *
192213 * @param exprs the expressions to combine with OR logic
193214 * @return Document representing $or operator
194215 */
@@ -197,30 +218,36 @@ object Agg {
197218 /* *
198219 * Builds a MongoDB aggregation expression to calculate the oldest (earliest) date from a nested array field.
199220 * Uses $reduce with $min to find the minimum date across all elements.
200- *
221+ *
201222 * @param inputField the input array field (e.g., "stories")
202223 * @param datePath the path to the date field within each element (e.g., "actions.date")
203224 * @return Document representing the min(date) expression
204225 */
205- private fun oldestDateInArray (inputField : String , datePath : String ): Document {
226+ private fun oldestDateInArray (
227+ inputField : String ,
228+ datePath : String ,
229+ ): Document {
206230 return dateInArray(inputField, datePath, ::min)
207231 }
208232
209233 /* *
210234 * Builds a MongoDB aggregation expression to calculate the youngest (latest) date from a nested array field.
211235 * Uses $reduce with $max to find the maximum date across all elements.
212- *
236+ *
213237 * @param inputField the input array field (e.g., "stories")
214238 * @param datePath the path to the date field within each element (e.g., "actions.date")
215239 * @return Document representing the max(date) expression
216240 */
217- private fun youngestDateInArray (inputField : String , datePath : String ): Document {
241+ private fun youngestDateInArray (
242+ inputField : String ,
243+ datePath : String ,
244+ ): Document {
218245 return dateInArray(inputField, datePath, ::max)
219246 }
220247
221248 /* *
222249 * Generic function to build a MongoDB aggregation expression for date aggregation in nested arrays.
223- *
250+ *
224251 * @param inputField the input array field (e.g., "stories")
225252 * @param datePath the path to the date field within each element (e.g., "actions.date")
226253 * @param aggregationFn the aggregation function to use (min for oldest, max for youngest)
@@ -229,34 +256,36 @@ object Agg {
229256 private fun dateInArray (
230257 inputField : String ,
231258 datePath : String ,
232- aggregationFn : (Any? ) -> Document
259+ aggregationFn : (Any? ) -> Document ,
233260 ): Document {
234261 return aggregationFn(
235262 reduce(
236263 input = field(inputField),
237264 initialValue = null ,
238- `in ` = cond(
239- ifExpr = eq(value(), null ),
240- thenExpr = ifNull(aggregationFn(" ${thisVar()} .$datePath " ), null ),
241- elseExpr = aggregationFn(
242- listOf (
243- value(),
244- ifNull(aggregationFn(" ${thisVar()} .$datePath " ), value())
245- )
246- )
247- )
248- )
265+ `in ` =
266+ cond(
267+ ifExpr = eq(value(), null ),
268+ thenExpr = ifNull(aggregationFn(" ${thisVar()} .$datePath " ), null ),
269+ elseExpr =
270+ aggregationFn(
271+ listOf (
272+ value(),
273+ ifNull(aggregationFn(" ${thisVar()} .$datePath " ), value()),
274+ ),
275+ ),
276+ ),
277+ ),
249278 )
250279 }
251280
252281 /* *
253282 * Filters documents where the oldest date (from array) is within a period.
254- *
283+ *
255284 * Condition: fromDate <= oldestDate <= toDate
256- *
285+ *
257286 * This is typically used for filtering by creation date, where we want to check
258287 * if the first action date falls within the specified period.
259- *
288+ *
260289 * @param inputField the input array field (e.g., "stories")
261290 * @param datePath the path to the date field within each element (e.g., "actions.date")
262291 * @param fromDate optional start date filter (inclusive)
@@ -267,19 +296,20 @@ object Agg {
267296 inputField : String ,
268297 datePath : String ,
269298 fromDate : ZonedDateTime ? ,
270- toDate : ZonedDateTime ?
299+ toDate : ZonedDateTime ? ,
271300 ): Bson ? {
272301 val oldestDateExpr = oldestDateInArray(inputField, datePath)
273- val conditionBuilders = listOfNotNull(
274- fromDate?.let { { gte(oldestDateExpr, it.toInstant()) } },
275- toDate?.let { { lte(oldestDateExpr, it.toInstant()) } }
276- )
302+ val conditionBuilders =
303+ listOfNotNull(
304+ fromDate?.let { { gte(oldestDateExpr, it.toInstant()) } },
305+ toDate?.let { { lte(oldestDateExpr, it.toInstant()) } },
306+ )
277307 return buildExprFromConditionBuilders(conditionBuilders)
278308 }
279309
280310 /* *
281311 * Builds a MongoDB $expr expression from a list of condition builders.
282- *
312+ *
283313 * @param conditionBuilders list of functions that build condition documents
284314 * @return Bson expression wrapping the conditions, or null if empty
285315 */
@@ -296,12 +326,12 @@ object Agg {
296326
297327 /* *
298328 * Filters documents where the activity period overlaps with the filter period.
299- *
329+ *
300330 * Condition: fromDate <= youngestDate AND oldestDate < toDate
301- *
331+ *
302332 * A document is included if its activity period (from oldest to youngest date)
303333 * overlaps the filter range. This implements period overlap logic.
304- *
334+ *
305335 * @param inputField the input array field (e.g., "stories")
306336 * @param datePath the path to the date field within each element (e.g., "actions.date")
307337 * @param fromDate optional start date filter (inclusive)
@@ -312,20 +342,19 @@ object Agg {
312342 inputField : String ,
313343 datePath : String ,
314344 fromDate : ZonedDateTime ? ,
315- toDate : ZonedDateTime ?
345+ toDate : ZonedDateTime ? ,
316346 ): Bson ? {
317- val conditionBuilders = listOfNotNull(
318- // fromDate <= youngestDate (need youngest only if fromDate is set)
319- fromDate?.let {
320- { gte(youngestDateInArray(inputField, datePath), it.toInstant()) }
321- },
322- // oldestDate < toDate (need oldest only if toDate is set)
323- toDate?.let {
324- { lt(oldestDateInArray(inputField, datePath), it.toInstant()) }
325- }
326- )
347+ val conditionBuilders =
348+ listOfNotNull(
349+ // fromDate <= youngestDate (need youngest only if fromDate is set)
350+ fromDate?.let {
351+ { gte(youngestDateInArray(inputField, datePath), it.toInstant()) }
352+ },
353+ // oldestDate < toDate (need oldest only if toDate is set)
354+ toDate?.let {
355+ { lt(oldestDateInArray(inputField, datePath), it.toInstant()) }
356+ },
357+ )
327358 return buildExprFromConditionBuilders(conditionBuilders)
328359 }
329-
330360}
331-
0 commit comments