@proofkit/fmodata 0.1.0-alpha.8 → 0.1.0-beta.23

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/LICENSE.md +21 -0
  2. package/README.md +651 -449
  3. package/dist/esm/client/batch-builder.d.ts +10 -9
  4. package/dist/esm/client/batch-builder.js +119 -56
  5. package/dist/esm/client/batch-builder.js.map +1 -1
  6. package/dist/esm/client/batch-request.js +16 -21
  7. package/dist/esm/client/batch-request.js.map +1 -1
  8. package/dist/esm/client/builders/default-select.d.ts +10 -0
  9. package/dist/esm/client/builders/default-select.js +41 -0
  10. package/dist/esm/client/builders/default-select.js.map +1 -0
  11. package/dist/esm/client/builders/expand-builder.d.ts +45 -0
  12. package/dist/esm/client/builders/expand-builder.js +185 -0
  13. package/dist/esm/client/builders/expand-builder.js.map +1 -0
  14. package/dist/esm/client/builders/index.d.ts +9 -0
  15. package/dist/esm/client/builders/query-string-builder.d.ts +18 -0
  16. package/dist/esm/client/builders/query-string-builder.js +21 -0
  17. package/dist/esm/client/builders/query-string-builder.js.map +1 -0
  18. package/dist/esm/client/builders/response-processor.d.ts +43 -0
  19. package/dist/esm/client/builders/response-processor.js +175 -0
  20. package/dist/esm/client/builders/response-processor.js.map +1 -0
  21. package/dist/esm/client/builders/select-mixin.d.ts +25 -0
  22. package/dist/esm/client/builders/select-mixin.js +28 -0
  23. package/dist/esm/client/builders/select-mixin.js.map +1 -0
  24. package/dist/esm/client/builders/select-utils.d.ts +18 -0
  25. package/dist/esm/client/builders/select-utils.js +30 -0
  26. package/dist/esm/client/builders/select-utils.js.map +1 -0
  27. package/dist/esm/client/builders/shared-types.d.ts +40 -0
  28. package/dist/esm/client/builders/table-utils.d.ts +35 -0
  29. package/dist/esm/client/builders/table-utils.js +44 -0
  30. package/dist/esm/client/builders/table-utils.js.map +1 -0
  31. package/dist/esm/client/database.d.ts +34 -22
  32. package/dist/esm/client/database.js +48 -84
  33. package/dist/esm/client/database.js.map +1 -1
  34. package/dist/esm/client/delete-builder.d.ts +25 -30
  35. package/dist/esm/client/delete-builder.js +45 -30
  36. package/dist/esm/client/delete-builder.js.map +1 -1
  37. package/dist/esm/client/entity-set.d.ts +35 -43
  38. package/dist/esm/client/entity-set.js +110 -52
  39. package/dist/esm/client/entity-set.js.map +1 -1
  40. package/dist/esm/client/error-parser.d.ts +12 -0
  41. package/dist/esm/client/error-parser.js +25 -0
  42. package/dist/esm/client/error-parser.js.map +1 -0
  43. package/dist/esm/client/filemaker-odata.d.ts +26 -7
  44. package/dist/esm/client/filemaker-odata.js +65 -42
  45. package/dist/esm/client/filemaker-odata.js.map +1 -1
  46. package/dist/esm/client/insert-builder.d.ts +19 -24
  47. package/dist/esm/client/insert-builder.js +94 -58
  48. package/dist/esm/client/insert-builder.js.map +1 -1
  49. package/dist/esm/client/query/expand-builder.d.ts +35 -0
  50. package/dist/esm/client/query/index.d.ts +4 -0
  51. package/dist/esm/client/query/query-builder.d.ts +132 -0
  52. package/dist/esm/client/query/query-builder.js +456 -0
  53. package/dist/esm/client/query/query-builder.js.map +1 -0
  54. package/dist/esm/client/query/response-processor.d.ts +25 -0
  55. package/dist/esm/client/query/types.d.ts +77 -0
  56. package/dist/esm/client/query/url-builder.d.ts +71 -0
  57. package/dist/esm/client/query/url-builder.js +100 -0
  58. package/dist/esm/client/query/url-builder.js.map +1 -0
  59. package/dist/esm/client/query-builder.d.ts +2 -115
  60. package/dist/esm/client/record-builder.d.ts +108 -36
  61. package/dist/esm/client/record-builder.js +284 -119
  62. package/dist/esm/client/record-builder.js.map +1 -1
  63. package/dist/esm/client/response-processor.d.ts +4 -9
  64. package/dist/esm/client/sanitize-json.d.ts +35 -0
  65. package/dist/esm/client/sanitize-json.js +27 -0
  66. package/dist/esm/client/sanitize-json.js.map +1 -0
  67. package/dist/esm/client/schema-manager.d.ts +5 -5
  68. package/dist/esm/client/schema-manager.js +45 -31
  69. package/dist/esm/client/schema-manager.js.map +1 -1
  70. package/dist/esm/client/update-builder.d.ts +34 -40
  71. package/dist/esm/client/update-builder.js +99 -58
  72. package/dist/esm/client/update-builder.js.map +1 -1
  73. package/dist/esm/client/webhook-builder.d.ts +126 -0
  74. package/dist/esm/client/webhook-builder.js +189 -0
  75. package/dist/esm/client/webhook-builder.js.map +1 -0
  76. package/dist/esm/errors.d.ts +19 -2
  77. package/dist/esm/errors.js +39 -4
  78. package/dist/esm/errors.js.map +1 -1
  79. package/dist/esm/index.d.ts +10 -8
  80. package/dist/esm/index.js +40 -10
  81. package/dist/esm/index.js.map +1 -1
  82. package/dist/esm/logger.d.ts +47 -0
  83. package/dist/esm/logger.js +69 -0
  84. package/dist/esm/logger.js.map +1 -0
  85. package/dist/esm/logger.test.d.ts +1 -0
  86. package/dist/esm/orm/column.d.ts +62 -0
  87. package/dist/esm/orm/column.js +63 -0
  88. package/dist/esm/orm/column.js.map +1 -0
  89. package/dist/esm/orm/field-builders.d.ts +164 -0
  90. package/dist/esm/orm/field-builders.js +158 -0
  91. package/dist/esm/orm/field-builders.js.map +1 -0
  92. package/dist/esm/orm/index.d.ts +5 -0
  93. package/dist/esm/orm/operators.d.ts +173 -0
  94. package/dist/esm/orm/operators.js +260 -0
  95. package/dist/esm/orm/operators.js.map +1 -0
  96. package/dist/esm/orm/table.d.ts +355 -0
  97. package/dist/esm/orm/table.js +202 -0
  98. package/dist/esm/orm/table.js.map +1 -0
  99. package/dist/esm/transform.d.ts +20 -21
  100. package/dist/esm/transform.js +44 -45
  101. package/dist/esm/transform.js.map +1 -1
  102. package/dist/esm/types.d.ts +96 -30
  103. package/dist/esm/types.js +7 -0
  104. package/dist/esm/types.js.map +1 -0
  105. package/dist/esm/validation.d.ts +22 -12
  106. package/dist/esm/validation.js +132 -85
  107. package/dist/esm/validation.js.map +1 -1
  108. package/package.json +28 -20
  109. package/src/client/batch-builder.ts +153 -89
  110. package/src/client/batch-request.ts +25 -41
  111. package/src/client/builders/default-select.ts +75 -0
  112. package/src/client/builders/expand-builder.ts +246 -0
  113. package/src/client/builders/index.ts +11 -0
  114. package/src/client/builders/query-string-builder.ts +46 -0
  115. package/src/client/builders/response-processor.ts +279 -0
  116. package/src/client/builders/select-mixin.ts +65 -0
  117. package/src/client/builders/select-utils.ts +59 -0
  118. package/src/client/builders/shared-types.ts +45 -0
  119. package/src/client/builders/table-utils.ts +83 -0
  120. package/src/client/database.ts +89 -183
  121. package/src/client/delete-builder.ts +74 -84
  122. package/src/client/entity-set.ts +266 -293
  123. package/src/client/error-parser.ts +41 -0
  124. package/src/client/filemaker-odata.ts +98 -66
  125. package/src/client/insert-builder.ts +157 -118
  126. package/src/client/query/expand-builder.ts +160 -0
  127. package/src/client/query/index.ts +14 -0
  128. package/src/client/query/query-builder.ts +729 -0
  129. package/src/client/query/response-processor.ts +226 -0
  130. package/src/client/query/types.ts +126 -0
  131. package/src/client/query/url-builder.ts +151 -0
  132. package/src/client/query-builder.ts +10 -1455
  133. package/src/client/record-builder.ts +575 -240
  134. package/src/client/response-processor.ts +15 -42
  135. package/src/client/sanitize-json.ts +64 -0
  136. package/src/client/schema-manager.ts +61 -76
  137. package/src/client/update-builder.ts +161 -143
  138. package/src/client/webhook-builder.ts +265 -0
  139. package/src/errors.ts +49 -16
  140. package/src/index.ts +99 -54
  141. package/src/logger.test.ts +34 -0
  142. package/src/logger.ts +116 -0
  143. package/src/orm/column.ts +106 -0
  144. package/src/orm/field-builders.ts +250 -0
  145. package/src/orm/index.ts +61 -0
  146. package/src/orm/operators.ts +473 -0
  147. package/src/orm/table.ts +741 -0
  148. package/src/transform.ts +90 -70
  149. package/src/types.ts +154 -113
  150. package/src/validation.ts +200 -115
  151. package/dist/esm/client/base-table.d.ts +0 -125
  152. package/dist/esm/client/base-table.js +0 -57
  153. package/dist/esm/client/base-table.js.map +0 -1
  154. package/dist/esm/client/query-builder.js +0 -896
  155. package/dist/esm/client/query-builder.js.map +0 -1
  156. package/dist/esm/client/table-occurrence.d.ts +0 -72
  157. package/dist/esm/client/table-occurrence.js +0 -74
  158. package/dist/esm/client/table-occurrence.js.map +0 -1
  159. package/dist/esm/filter-types.d.ts +0 -76
  160. package/src/client/base-table.ts +0 -166
  161. package/src/client/query-builder.ts.bak +0 -1457
  162. package/src/client/table-occurrence.ts +0 -175
  163. package/src/filter-types.ts +0 -97
@@ -0,0 +1,473 @@
1
+ import { needsFieldQuoting } from "../client/builders/select-utils";
2
+ import type { Column } from "./column";
3
+ import { isColumn } from "./column";
4
+
5
+ /**
6
+ * FilterExpression represents a filter condition that can be used in where() clauses.
7
+ * Internal representation of operator expressions that get converted to OData filter syntax.
8
+ */
9
+ export class FilterExpression {
10
+ readonly operator: string;
11
+ // biome-ignore lint/suspicious/noExplicitAny: Operands can be Column, FilterExpression, or any value type
12
+ readonly operands: (Column | any | FilterExpression)[];
13
+
14
+ // biome-ignore lint/suspicious/noExplicitAny: Operands can be Column, FilterExpression, or any value type
15
+ constructor(operator: string, operands: (Column | any | FilterExpression)[]) {
16
+ this.operator = operator;
17
+ this.operands = operands;
18
+ }
19
+
20
+ /**
21
+ * Convert this expression to OData filter syntax.
22
+ * @internal Used by QueryBuilder
23
+ */
24
+ toODataFilter(useEntityIds?: boolean): string {
25
+ switch (this.operator) {
26
+ // Comparison operators
27
+ case "eq":
28
+ return this._binaryOp("eq", useEntityIds);
29
+ case "ne":
30
+ return this._binaryOp("ne", useEntityIds);
31
+ case "gt":
32
+ return this._binaryOp("gt", useEntityIds);
33
+ case "gte":
34
+ return this._binaryOp("ge", useEntityIds);
35
+ case "lt":
36
+ return this._binaryOp("lt", useEntityIds);
37
+ case "lte":
38
+ return this._binaryOp("le", useEntityIds);
39
+ case "in":
40
+ return this._inOp(useEntityIds);
41
+ case "notIn":
42
+ return this._notInOp(useEntityIds);
43
+
44
+ // String operators
45
+ case "contains":
46
+ return this._functionOp("contains", useEntityIds);
47
+ case "startsWith":
48
+ return this._functionOp("startswith", useEntityIds);
49
+ case "endsWith":
50
+ return this._functionOp("endswith", useEntityIds);
51
+
52
+ // Null checks
53
+ case "isNull":
54
+ return this._isNullOp(useEntityIds);
55
+ case "isNotNull":
56
+ return this._isNotNullOp(useEntityIds);
57
+
58
+ // Logical operators
59
+ case "and":
60
+ return this._logicalOp("and", useEntityIds);
61
+ case "or":
62
+ return this._logicalOp("or", useEntityIds);
63
+ case "not":
64
+ return this._notOp(useEntityIds);
65
+
66
+ default:
67
+ throw new Error(`Unknown operator: ${this.operator}`);
68
+ }
69
+ }
70
+
71
+ private _binaryOp(op: string, useEntityIds?: boolean): string {
72
+ const [left, right] = this.operands;
73
+ // For binary ops, the column is typically the first operand and value is the second
74
+ // But we also support column-to-column comparisons, so check both
75
+ let columnForValue: typeof left | typeof right | undefined;
76
+ if (isColumn(left) && !isColumn(right)) {
77
+ columnForValue = left;
78
+ } else if (isColumn(right) && !isColumn(left)) {
79
+ columnForValue = right;
80
+ } else {
81
+ columnForValue = undefined;
82
+ }
83
+ const leftStr = this._operandToString(left, useEntityIds, columnForValue);
84
+ const rightStr = this._operandToString(right, useEntityIds, columnForValue);
85
+ return `${leftStr} ${op} ${rightStr}`;
86
+ }
87
+
88
+ private _functionOp(fnName: string, useEntityIds?: boolean): string {
89
+ const [column, value] = this.operands;
90
+ const columnInstance = isColumn(column) ? column : undefined;
91
+ const columnStr = this._operandToString(column, useEntityIds);
92
+ const valueStr = this._operandToString(value, useEntityIds, columnInstance);
93
+ return `${fnName}(${columnStr}, ${valueStr})`;
94
+ }
95
+
96
+ private _inOp(useEntityIds?: boolean): string {
97
+ const [column, values] = this.operands;
98
+ const columnInstance = isColumn(column) ? column : undefined;
99
+ const columnStr = this._operandToString(column, useEntityIds);
100
+ // biome-ignore lint/suspicious/noExplicitAny: Dynamic array of values from user input
101
+ const valuesStr = (values as any[]).map((v) => this._operandToString(v, useEntityIds, columnInstance)).join(", ");
102
+ return `${columnStr} in (${valuesStr})`;
103
+ }
104
+
105
+ private _notInOp(useEntityIds?: boolean): string {
106
+ const [column, values] = this.operands;
107
+ const columnInstance = isColumn(column) ? column : undefined;
108
+ const columnStr = this._operandToString(column, useEntityIds);
109
+ // biome-ignore lint/suspicious/noExplicitAny: Dynamic array of values from user input
110
+ const valuesStr = (values as any[]).map((v) => this._operandToString(v, useEntityIds, columnInstance)).join(", ");
111
+ return `not (${columnStr} in (${valuesStr}))`;
112
+ }
113
+
114
+ private _isNullOp(useEntityIds?: boolean): string {
115
+ const [column] = this.operands;
116
+ const columnStr = this._operandToString(column, useEntityIds);
117
+ return `${columnStr} eq null`;
118
+ }
119
+
120
+ private _isNotNullOp(useEntityIds?: boolean): string {
121
+ const [column] = this.operands;
122
+ const columnStr = this._operandToString(column, useEntityIds);
123
+ return `${columnStr} ne null`;
124
+ }
125
+
126
+ private _logicalOp(op: string, useEntityIds?: boolean): string {
127
+ const expressions = this.operands.map((expr) => {
128
+ if (expr instanceof FilterExpression) {
129
+ const innerExpr = expr.toODataFilter(useEntityIds);
130
+ // Wrap in parens if it's a logical expression to ensure precedence
131
+ if (expr.operator === "and" || expr.operator === "or") {
132
+ return `(${innerExpr})`;
133
+ }
134
+ return innerExpr;
135
+ }
136
+ throw new Error("Logical operators require FilterExpression operands");
137
+ });
138
+ return expressions.join(` ${op} `);
139
+ }
140
+
141
+ private _notOp(useEntityIds?: boolean): string {
142
+ const [expr] = this.operands;
143
+ if (expr instanceof FilterExpression) {
144
+ return `not (${expr.toODataFilter(useEntityIds)})`;
145
+ }
146
+ throw new Error("NOT operator requires a FilterExpression operand");
147
+ }
148
+
149
+ private _operandToString(
150
+ // biome-ignore lint/suspicious/noExplicitAny: Operand can be Column, FilterExpression, or any value type
151
+ operand: any,
152
+ useEntityIds?: boolean, // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration
153
+ column?: Column<any, any, any, any>,
154
+ ): string {
155
+ if (isColumn(operand)) {
156
+ const fieldIdentifier = operand.getFieldIdentifier(useEntityIds);
157
+ // Quote field names in OData filters per FileMaker OData API requirements
158
+ return needsFieldQuoting(fieldIdentifier) ? `"${fieldIdentifier}"` : fieldIdentifier;
159
+ }
160
+
161
+ // If we have a column with an input validator, apply it to transform the value
162
+ let value = operand;
163
+ if (column?.inputValidator) {
164
+ try {
165
+ const result = column.inputValidator["~standard"].validate(value);
166
+ // Handle async validators (though they shouldn't be async for filters)
167
+ if (result instanceof Promise) {
168
+ // For filters, we can't use async validators, so skip transformation
169
+ // This is a limitation - async validators won't work in filters
170
+ value = operand;
171
+ } else if ("issues" in result && result.issues) {
172
+ // Validation failed, use original value
173
+ value = operand;
174
+ } else if ("value" in result) {
175
+ // Validation succeeded, use transformed value
176
+ value = result.value;
177
+ }
178
+ } catch (_error) {
179
+ // If validation throws, use the original value (will likely cause a query error)
180
+ // This maintains backward compatibility and allows the server to handle validation
181
+ value = operand;
182
+ }
183
+ }
184
+
185
+ if (typeof value === "string") {
186
+ return `'${value.replace(/'/g, "''")}'`; // Escape single quotes
187
+ }
188
+ if (value === null || value === undefined) {
189
+ return "null";
190
+ }
191
+ if (value instanceof Date) {
192
+ return value.toISOString();
193
+ }
194
+ return String(value);
195
+ }
196
+ }
197
+
198
+ // ============================================================================
199
+ // Comparison Operators
200
+ // ============================================================================
201
+
202
+ /**
203
+ * Equal operator - checks if column equals a value or another column.
204
+ *
205
+ * @example
206
+ * eq(users.name, "John") // name equals "John"
207
+ * eq(users.id, contacts.id_user) // cross-table comparison
208
+ */
209
+ export function eq<TOutput, TInput>(
210
+ column1: Column<TOutput, TInput>,
211
+ column2: Column<TOutput, TInput> | NoInfer<TInput>,
212
+ ): FilterExpression;
213
+ // biome-ignore lint/suspicious/noExplicitAny: Implementation signature for overloads
214
+ export function eq(column: Column, value: any): FilterExpression {
215
+ return new FilterExpression("eq", [column, value]);
216
+ }
217
+
218
+ /**
219
+ * Not equal operator - checks if column does not equal a value or another column.
220
+ *
221
+ * @example
222
+ * ne(users.status, "inactive") // status not equal to "inactive"
223
+ * ne(users.id, contacts.id_user) // cross-table comparison
224
+ */
225
+ export function ne<TOutput, TInput>(
226
+ column1: Column<TOutput, TInput>,
227
+ column2: Column<TOutput, TInput> | NoInfer<TInput>,
228
+ ): FilterExpression;
229
+ // biome-ignore lint/suspicious/noExplicitAny: Implementation signature for overloads
230
+ export function ne(column: Column, value: any): FilterExpression {
231
+ return new FilterExpression("ne", [column, value]);
232
+ }
233
+
234
+ /**
235
+ * Greater than operator - checks if column is greater than a value.
236
+ *
237
+ * @example
238
+ * gt(users.age, 18) // age greater than 18
239
+ */
240
+ export function gt<TOutput extends number | string | Date | null, TInput>(
241
+ column: Column<TOutput, TInput>,
242
+ value: NoInfer<TInput>,
243
+ ): FilterExpression {
244
+ return new FilterExpression("gt", [column, value]);
245
+ }
246
+
247
+ /**
248
+ * Greater than or equal operator - checks if column is >= a value.
249
+ *
250
+ * @example
251
+ * gte(users.age, 18) // age >= 18
252
+ */
253
+ export function gte<TOutput extends number | string | Date | null, TInput>(
254
+ column: Column<TOutput, TInput>,
255
+ value: NoInfer<TInput>,
256
+ ): FilterExpression {
257
+ return new FilterExpression("gte", [column, value]);
258
+ }
259
+
260
+ /**
261
+ * Less than operator - checks if column is less than a value.
262
+ *
263
+ * @example
264
+ * lt(users.age, 65) // age less than 65
265
+ */
266
+ export function lt<TOutput extends number | string | Date | null, TInput>(
267
+ column: Column<TOutput, TInput>,
268
+ value: NoInfer<TInput>,
269
+ ): FilterExpression {
270
+ return new FilterExpression("lt", [column, value]);
271
+ }
272
+
273
+ /**
274
+ * Less than or equal operator - checks if column is <= a value.
275
+ *
276
+ * @example
277
+ * lte(users.age, 65) // age <= 65
278
+ */
279
+ export function lte<TOutput extends number | string | Date | null, TInput>(
280
+ column: Column<TOutput, TInput>,
281
+ value: NoInfer<TInput>,
282
+ ): FilterExpression {
283
+ return new FilterExpression("lte", [column, value]);
284
+ }
285
+
286
+ // ============================================================================
287
+ // String Operators
288
+ // ============================================================================
289
+
290
+ /**
291
+ * Contains operator - checks if a string column contains a substring.
292
+ *
293
+ * @example
294
+ * contains(users.name, "John") // name contains "John"
295
+ */
296
+ export function contains<TOutput, TInput>(column: Column<TOutput, TInput>, value: NoInfer<TInput>): FilterExpression {
297
+ return new FilterExpression("contains", [column, value]);
298
+ }
299
+
300
+ /**
301
+ * Starts with operator - checks if a string column starts with a prefix.
302
+ *
303
+ * @example
304
+ * startsWith(users.email, "admin") // email starts with "admin"
305
+ */
306
+ export function startsWith<TOutput, TInput>(column: Column<TOutput, TInput>, value: NoInfer<TInput>): FilterExpression {
307
+ return new FilterExpression("startsWith", [column, value]);
308
+ }
309
+
310
+ /**
311
+ * Ends with operator - checks if a string column ends with a suffix.
312
+ *
313
+ * @example
314
+ * endsWith(users.email, "@example.com") // email ends with "@example.com"
315
+ */
316
+ export function endsWith<TOutput, TInput>(column: Column<TOutput, TInput>, value: NoInfer<TInput>): FilterExpression {
317
+ return new FilterExpression("endsWith", [column, value]);
318
+ }
319
+
320
+ // ============================================================================
321
+ // Array Operators
322
+ // ============================================================================
323
+
324
+ /**
325
+ * In array operator - checks if column value is in an array of values.
326
+ *
327
+ * @example
328
+ * inArray(users.status, ["active", "pending"]) // status is "active" or "pending"
329
+ */
330
+ export function inArray<TOutput, TInput>(column: Column<TOutput, TInput>, values: NoInfer<TInput>[]): FilterExpression {
331
+ return new FilterExpression("in", [column, values]);
332
+ }
333
+
334
+ /**
335
+ * Not in array operator - checks if column value is not in an array of values.
336
+ *
337
+ * @example
338
+ * notInArray(users.status, ["deleted", "banned"]) // status is neither "deleted" nor "banned"
339
+ */
340
+ export function notInArray<TOutput, TInput>(
341
+ column: Column<TOutput, TInput>,
342
+ values: NoInfer<TInput>[],
343
+ ): FilterExpression {
344
+ return new FilterExpression("notIn", [column, values]);
345
+ }
346
+
347
+ // ============================================================================
348
+ // Null Check Operators
349
+ // ============================================================================
350
+
351
+ /**
352
+ * Is null operator - checks if column value is null.
353
+ *
354
+ * @example
355
+ * isNull(users.deletedAt) // deletedAt is null
356
+ */
357
+ export function isNull<TOutput, TInput>(column: Column<TOutput, TInput>): FilterExpression {
358
+ return new FilterExpression("isNull", [column]);
359
+ }
360
+
361
+ /**
362
+ * Is not null operator - checks if column value is not null.
363
+ *
364
+ * @example
365
+ * isNotNull(users.email) // email is not null
366
+ */
367
+ export function isNotNull<TOutput, TInput>(column: Column<TOutput, TInput>): FilterExpression {
368
+ return new FilterExpression("isNotNull", [column]);
369
+ }
370
+
371
+ // ============================================================================
372
+ // Logical Operators
373
+ // ============================================================================
374
+
375
+ /**
376
+ * AND operator - combines multiple filter expressions with logical AND.
377
+ * All expressions must be true for the record to match.
378
+ *
379
+ * @example
380
+ * and(
381
+ * eq(users.active, true),
382
+ * gt(users.age, 18)
383
+ * ) // active is true AND age > 18
384
+ */
385
+ export function and(...expressions: FilterExpression[]): FilterExpression {
386
+ if (expressions.length === 0) {
387
+ throw new Error("AND operator requires at least one expression");
388
+ }
389
+ if (expressions.length === 1 && expressions[0] !== undefined) {
390
+ return expressions[0];
391
+ }
392
+ return new FilterExpression("and", expressions);
393
+ }
394
+
395
+ /**
396
+ * OR operator - combines multiple filter expressions with logical OR.
397
+ * At least one expression must be true for the record to match.
398
+ *
399
+ * @example
400
+ * or(
401
+ * eq(users.role, "admin"),
402
+ * eq(users.role, "moderator")
403
+ * ) // role is "admin" OR "moderator"
404
+ */
405
+ export function or(...expressions: FilterExpression[]): FilterExpression {
406
+ if (expressions.length === 0) {
407
+ throw new Error("OR operator requires at least one expression");
408
+ }
409
+ if (expressions.length === 1 && expressions[0] !== undefined) {
410
+ return expressions[0];
411
+ }
412
+ return new FilterExpression("or", expressions);
413
+ }
414
+
415
+ /**
416
+ * NOT operator - negates a filter expression.
417
+ *
418
+ * @example
419
+ * not(eq(users.status, "deleted")) // status is NOT "deleted"
420
+ */
421
+ export function not(expression: FilterExpression): FilterExpression {
422
+ return new FilterExpression("not", [expression]);
423
+ }
424
+
425
+ // ============================================================================
426
+ // OrderBy Operators
427
+ // ============================================================================
428
+
429
+ /**
430
+ * OrderByExpression represents a sort order specification for a column.
431
+ * Used in orderBy() clauses to provide type-safe sorting with direction.
432
+ */
433
+ export class OrderByExpression<TableName extends string = string> {
434
+ // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration
435
+ readonly column: Column<any, any, TableName>;
436
+ readonly direction: "asc" | "desc";
437
+
438
+ // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration
439
+ constructor(column: Column<any, any, TableName>, direction: "asc" | "desc") {
440
+ this.column = column;
441
+ this.direction = direction;
442
+ }
443
+ }
444
+
445
+ /**
446
+ * Type guard to check if a value is an OrderByExpression instance.
447
+ */
448
+ // biome-ignore lint/suspicious/noExplicitAny: Type guard accepting any value type
449
+ export function isOrderByExpression(value: any): value is OrderByExpression {
450
+ return value instanceof OrderByExpression;
451
+ }
452
+
453
+ /**
454
+ * Ascending order operator - sorts a column in ascending order.
455
+ *
456
+ * @example
457
+ * asc(users.name) // Sort by name ascending
458
+ */
459
+ // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration
460
+ export function asc<TableName extends string>(column: Column<any, any, TableName>): OrderByExpression<TableName> {
461
+ return new OrderByExpression(column, "asc");
462
+ }
463
+
464
+ /**
465
+ * Descending order operator - sorts a column in descending order.
466
+ *
467
+ * @example
468
+ * desc(users.age) // Sort by age descending
469
+ */
470
+ // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration
471
+ export function desc<TableName extends string>(column: Column<any, any, TableName>): OrderByExpression<TableName> {
472
+ return new OrderByExpression(column, "desc");
473
+ }