@bagelink/vue 1.7.80 → 1.7.86

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.
@@ -0,0 +1,366 @@
1
+ /**
2
+ * SCIM 2.0 Query Builder
3
+ *
4
+ * Type-safe query builder for SCIM 2.0 filtering.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * // Array-based (recommended for UI)
9
+ * const filters: QueryConditions<Person> = [
10
+ * { field: 'first_name', op: 'eq', value: 'John' },
11
+ * { field: 'age', op: 'gt', value: 18, connector: 'and' },
12
+ * ]
13
+ * buildQuery(filters) // → 'first_name eq "John" and age gt 18'
14
+ *
15
+ * // Range query (same field twice)
16
+ * const rangeFilters: QueryConditions<Donation> = [
17
+ * { field: 'amount', op: 'ge', value: 100 },
18
+ * { field: 'amount', op: 'le', value: 500, connector: 'and' },
19
+ * ]
20
+ * buildQuery(rangeFilters) // → 'amount ge 100 and amount le 500'
21
+ * ```
22
+ */
23
+
24
+ type Primitive = string | number | boolean | null
25
+
26
+ type DeepKeyOf<T> = T extends Primitive
27
+ ? never
28
+ : {
29
+ [K in keyof T & string]: K | `${K}.${DeepKeyOf<T[K]>}`;
30
+ }[keyof T & string]
31
+
32
+ /**
33
+ * SCIM 2.0 comparison operators
34
+ */
35
+ export type ComparisonOperator =
36
+ | 'eq' // equal
37
+ | 'ne' // not equal
38
+ | 'gt' // greater than
39
+ | 'ge' // greater than or equal
40
+ | 'lt' // less than
41
+ | 'le' // less than or equal
42
+ | 'co' // contains
43
+ | 'sw' // starts with
44
+ | 'ew' // ends with
45
+ | 'pr' // present (has value)
46
+
47
+ /**
48
+ * SCIM 2.0 logical operators
49
+ */
50
+ export type LogicalOperator = 'and' | 'or'
51
+
52
+ /**
53
+ * A single filter condition
54
+ */
55
+ export interface FilterCondition<T extends Record<string, any> = Record<string, any>> {
56
+ field: DeepKeyOf<T> | string
57
+ op: ComparisonOperator
58
+ value?: Primitive // optional for 'pr' operator
59
+ connector?: LogicalOperator // before this condition, ignored for first
60
+ }
61
+
62
+ /**
63
+ * Array of filter conditions - the primary way to define queries
64
+ */
65
+ export type QueryConditions<T extends Record<string, any> = Record<string, any>> = FilterCondition<T>[]
66
+
67
+ /**
68
+ * Format a value for SCIM 2.0 query
69
+ */
70
+ function formatValue(value: Primitive): string {
71
+ if (value === null) return 'null'
72
+ if (typeof value === 'string') {
73
+ const escaped = value.replace(/"/g, '\\"')
74
+ return `"${escaped}"`
75
+ }
76
+ if (typeof value === 'boolean') return value.toString()
77
+ return String(value)
78
+ }
79
+
80
+ /**
81
+ * Build a SCIM 2.0 query string from conditions array
82
+ *
83
+ * @example
84
+ * ```typescript
85
+ * buildQuery<Person>([
86
+ * { field: 'name', op: 'eq', value: 'John' },
87
+ * { field: 'age', op: 'gt', value: 18, connector: 'and' },
88
+ * ])
89
+ * // → 'name eq "John" and age gt 18'
90
+ * ```
91
+ */
92
+ export function buildQuery<T extends Record<string, any>>(
93
+ conditions: QueryConditions<T>
94
+ ): string {
95
+ if (!conditions || conditions.length === 0) return ''
96
+
97
+ const parts: string[] = []
98
+
99
+ for (let i = 0; i < conditions.length; i++) {
100
+ const { field, op, value, connector } = conditions[i]
101
+
102
+ // Add connector (except for first condition)
103
+ if (i > 0 && connector) {
104
+ parts.push(connector)
105
+ }
106
+
107
+ // Build the condition
108
+ if (op === 'pr') {
109
+ parts.push(`${field} pr`)
110
+ } else {
111
+ parts.push(`${field} ${op} ${formatValue(value ?? null)}`)
112
+ }
113
+ }
114
+
115
+ return parts.join(' ')
116
+ }
117
+
118
+ /**
119
+ * Query Builder for SCIM 2.0 filtering
120
+ */
121
+ export class QueryFilter<T extends Record<string, any>> {
122
+ private parts: string[] = []
123
+
124
+ /**
125
+ * Equal comparison
126
+ */
127
+ eq<K extends DeepKeyOf<T>>(field: K, value: Primitive): this {
128
+ this.parts.push(`${field as string} eq ${this.formatValue(value)}`)
129
+ return this
130
+ }
131
+
132
+ /**
133
+ * Not equal comparison
134
+ */
135
+ ne<K extends DeepKeyOf<T>>(field: K, value: Primitive): this {
136
+ this.parts.push(`${field as string} ne ${this.formatValue(value)}`)
137
+ return this
138
+ }
139
+
140
+ /**
141
+ * Greater than comparison
142
+ */
143
+ gt<K extends DeepKeyOf<T>>(field: K, value: number | string): this {
144
+ this.parts.push(`${field as string} gt ${this.formatValue(value)}`)
145
+ return this
146
+ }
147
+
148
+ /**
149
+ * Greater than or equal comparison
150
+ */
151
+ ge<K extends DeepKeyOf<T>>(field: K, value: number | string): this {
152
+ this.parts.push(`${field as string} ge ${this.formatValue(value)}`)
153
+ return this
154
+ }
155
+
156
+ /**
157
+ * Less than comparison
158
+ */
159
+ lt<K extends DeepKeyOf<T>>(field: K, value: number | string): this {
160
+ this.parts.push(`${field as string} lt ${this.formatValue(value)}`)
161
+ return this
162
+ }
163
+
164
+ /**
165
+ * Less than or equal comparison
166
+ */
167
+ le<K extends DeepKeyOf<T>>(field: K, value: number | string): this {
168
+ this.parts.push(`${field as string} le ${this.formatValue(value)}`)
169
+ return this
170
+ }
171
+
172
+ /**
173
+ * Contains comparison (for strings)
174
+ */
175
+ co<K extends DeepKeyOf<T>>(field: K, value: string): this {
176
+ this.parts.push(`${field as string} co ${this.formatValue(value)}`)
177
+ return this
178
+ }
179
+
180
+ /**
181
+ * Starts with comparison (for strings)
182
+ */
183
+ sw<K extends DeepKeyOf<T>>(field: K, value: string): this {
184
+ this.parts.push(`${field as string} sw ${this.formatValue(value)}`)
185
+ return this
186
+ }
187
+
188
+ /**
189
+ * Ends with comparison (for strings)
190
+ */
191
+ ew<K extends DeepKeyOf<T>>(field: K, value: string): this {
192
+ this.parts.push(`${field as string} ew ${this.formatValue(value)}`)
193
+ return this
194
+ }
195
+
196
+ /**
197
+ * Present check (field has a value)
198
+ */
199
+ pr<K extends DeepKeyOf<T>>(field: K): this {
200
+ this.parts.push(`${field as string} pr`)
201
+ return this
202
+ }
203
+
204
+ /**
205
+ * AND logical operator
206
+ */
207
+ and(): this {
208
+ this.parts.push('and')
209
+ return this
210
+ }
211
+
212
+ /**
213
+ * OR logical operator
214
+ */
215
+ or(): this {
216
+ this.parts.push('or')
217
+ return this
218
+ }
219
+
220
+ /**
221
+ * NOT logical operator
222
+ */
223
+ not(): this {
224
+ this.parts.push('not')
225
+ return this
226
+ }
227
+
228
+ /**
229
+ * Group expressions with parentheses
230
+ */
231
+ group(builderFn: (qb: QueryFilter<T>) => QueryFilter<T>): this {
232
+ const groupBuilder = new QueryFilter<T>()
233
+ builderFn(groupBuilder)
234
+ const groupQuery = groupBuilder.build()
235
+ if (groupQuery) {
236
+ this.parts.push(`(${groupQuery})`)
237
+ }
238
+ return this
239
+ }
240
+
241
+ /**
242
+ * Add raw query string (use with caution)
243
+ */
244
+ raw(query: string): this {
245
+ this.parts.push(query)
246
+ return this
247
+ }
248
+
249
+ /**
250
+ * Clear the query
251
+ */
252
+ clear(): this {
253
+ this.parts = []
254
+ return this
255
+ }
256
+
257
+ /**
258
+ * Build the final query string
259
+ */
260
+ build(): string {
261
+ return this.parts.join(' ')
262
+ }
263
+
264
+ /**
265
+ * Convert to string automatically (allows using builder without .build())
266
+ */
267
+ toString(): string {
268
+ return this.build()
269
+ }
270
+
271
+ /**
272
+ * Allow implicit string conversion
273
+ */
274
+ [Symbol.toPrimitive](hint: string): string | number {
275
+ if (hint === 'string') {
276
+ return this.build()
277
+ }
278
+ return this.parts.length
279
+ }
280
+
281
+ /**
282
+ * Format value for SCIM 2.0 query
283
+ */
284
+ private formatValue(value: Primitive): string {
285
+ if (value === null) {
286
+ return 'null'
287
+ }
288
+ if (typeof value === 'string') {
289
+ // Escape quotes in strings
290
+ const escaped = value.replace(/"/g, '\\"')
291
+ return `"${escaped}"`
292
+ }
293
+ if (typeof value === 'boolean') {
294
+ return value.toString()
295
+ }
296
+ return String(value)
297
+ }
298
+ }
299
+
300
+ /**
301
+ * Build a query string from conditions
302
+ *
303
+ * @example
304
+ * ```typescript
305
+ * // From conditions array
306
+ * query<Person>([
307
+ * { field: 'first_name', op: 'eq', value: 'John' },
308
+ * { field: 'age', op: 'gt', value: 18, connector: 'and' },
309
+ * ])
310
+ * // → 'first_name eq "John" and age gt 18'
311
+ *
312
+ * // Empty returns empty string
313
+ * query([]) // → ''
314
+ * ```
315
+ */
316
+ export function query<T extends Record<string, any>>(
317
+ conditions: QueryConditions<T>
318
+ ): string {
319
+ return buildQuery(conditions)
320
+ }
321
+
322
+ /**
323
+ * Create a query that checks if any of the fields match the value
324
+ */
325
+ export function anyOf<T extends Record<string, any>>(
326
+ fields: Array<DeepKeyOf<T> | string>,
327
+ value: Primitive
328
+ ): string {
329
+ const conditions: QueryConditions<T> = fields.map((field, i) => ({
330
+ field,
331
+ op: 'eq' as const,
332
+ value,
333
+ connector: i > 0 ? 'or' as const : undefined,
334
+ }))
335
+ return buildQuery(conditions)
336
+ }
337
+
338
+ /**
339
+ * Create a range query (field >= min and field <= max)
340
+ */
341
+ export function range<T extends Record<string, any>>(
342
+ field: DeepKeyOf<T> | string,
343
+ min: number | string,
344
+ max: number | string
345
+ ): string {
346
+ return buildQuery<T>([
347
+ { field, op: 'ge', value: min },
348
+ { field, op: 'le', value: max, connector: 'and' },
349
+ ])
350
+ }
351
+
352
+ /**
353
+ * Create a search query across multiple string fields (OR)
354
+ */
355
+ export function search<T extends Record<string, any>>(
356
+ fields: Array<DeepKeyOf<T> | string>,
357
+ searchTerm: string
358
+ ): string {
359
+ const conditions: QueryConditions<T> = fields.map((field, i) => ({
360
+ field,
361
+ op: 'co' as const,
362
+ value: searchTerm,
363
+ connector: i > 0 ? 'or' as const : undefined,
364
+ }))
365
+ return buildQuery(conditions)
366
+ }