node-condition-builder 1.0.0

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 (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +223 -0
  3. package/dist/ConditionBuilder.d.ts +39 -0
  4. package/dist/ConditionBuilder.d.ts.map +1 -0
  5. package/dist/ConditionBuilder.js +156 -0
  6. package/dist/Expression.d.ts +5 -0
  7. package/dist/Expression.d.ts.map +1 -0
  8. package/dist/Expression.js +6 -0
  9. package/dist/conditions/BetweenCondition.d.ts +11 -0
  10. package/dist/conditions/BetweenCondition.d.ts.map +1 -0
  11. package/dist/conditions/BetweenCondition.js +34 -0
  12. package/dist/conditions/ComparisonCondition.d.ts +10 -0
  13. package/dist/conditions/ComparisonCondition.d.ts.map +1 -0
  14. package/dist/conditions/ComparisonCondition.js +25 -0
  15. package/dist/conditions/Condition.d.ts +5 -0
  16. package/dist/conditions/Condition.d.ts.map +1 -0
  17. package/dist/conditions/Condition.js +2 -0
  18. package/dist/conditions/EqualCondition.d.ts +10 -0
  19. package/dist/conditions/EqualCondition.d.ts.map +1 -0
  20. package/dist/conditions/EqualCondition.js +26 -0
  21. package/dist/conditions/InCondition.d.ts +10 -0
  22. package/dist/conditions/InCondition.d.ts.map +1 -0
  23. package/dist/conditions/InCondition.js +31 -0
  24. package/dist/conditions/NullCondition.d.ts +9 -0
  25. package/dist/conditions/NullCondition.d.ts.map +1 -0
  26. package/dist/conditions/NullCondition.js +16 -0
  27. package/dist/conditions/RawCondition.d.ts +9 -0
  28. package/dist/conditions/RawCondition.d.ts.map +1 -0
  29. package/dist/conditions/RawCondition.js +21 -0
  30. package/dist/dialects.d.ts +3 -0
  31. package/dist/dialects.d.ts.map +1 -0
  32. package/dist/dialects.js +8 -0
  33. package/dist/index.d.ts +4 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +2 -0
  36. package/dist/types.d.ts +2 -0
  37. package/dist/types.d.ts.map +1 -0
  38. package/dist/types.js +1 -0
  39. package/package.json +44 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Luca Rainone (luca.rainone@gmail.com)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,223 @@
1
+ # ConditionBuilder
2
+
3
+ A lightweight TypeScript library for building parameterized SQL WHERE clauses. It generates safe, injection-free condition strings with proper placeholder indexing for PostgreSQL and MySQL.
4
+
5
+ ## What it is
6
+
7
+ A **condition builder** -- it takes your filter parameters and produces a SQL condition string plus an ordered array of values, ready to be passed to any database driver's parameterized query.
8
+
9
+ ## What it is NOT
10
+
11
+ - Not an ORM
12
+ - Not a query builder -- it only handles the WHERE clause
13
+ - Not a database driver -- it produces strings, you execute them
14
+
15
+ ## Use cases
16
+
17
+ - Building dynamic filters where conditions are optional
18
+ - APIs with search/filter endpoints where any combination of parameters may be present
19
+ - Anywhere you need safe, parameterized SQL conditions without string concatenation
20
+
21
+ ## Install
22
+
23
+ ```
24
+ npm install node-condition-builder
25
+ ```
26
+
27
+ ## Quick start
28
+
29
+ ```typescript
30
+ import { ConditionBuilder } from 'node-condition-builder';
31
+
32
+ const condition = new ConditionBuilder('AND')
33
+ .isEqual('status', 'active')
34
+ .isGreater('age', 18)
35
+ .isNull('deleted_at', true);
36
+
37
+ condition.build(); // (status = $1 AND age > $2 AND deleted_at IS NULL)
38
+ condition.getValues(); // ['active', 18]
39
+ ```
40
+
41
+ ## Undefined values are ignored
42
+
43
+ This is the key feature. When a value is `undefined`, the condition is silently skipped. This makes it trivial to build dynamic filters from optional parameters:
44
+
45
+ ```typescript
46
+ interface UserFilters {
47
+ name?: string;
48
+ email?: string;
49
+ role?: string;
50
+ minAge?: number;
51
+ maxAge?: number;
52
+ isVerified?: boolean;
53
+ excludeRoles?: string[];
54
+ }
55
+
56
+ function filterUsers(filters: UserFilters) {
57
+ const condition = new ConditionBuilder('AND')
58
+ .isLike('name', filters.name ? `%${filters.name}%` : undefined)
59
+ .isEqual('email', filters.email)
60
+ .isEqual('role', filters.role)
61
+ .isGreaterOrEqual('age', filters.minAge)
62
+ .isLessOrEqual('age', filters.maxAge)
63
+ .isNotNull('verified_at', filters.isVerified)
64
+ .isNotIn('role', filters.excludeRoles);
65
+
66
+ const sql = `SELECT * FROM users WHERE ${condition.build()}`;
67
+ const values = condition.getValues();
68
+
69
+ return db.query(sql, values);
70
+ }
71
+
72
+ // Only name and minAge provided:
73
+ filterUsers({ name: 'john', minAge: 18 });
74
+ // SELECT * FROM users WHERE (name LIKE $1 AND age >= $2)
75
+ // values: ['%john%', 18]
76
+
77
+ // No filters at all:
78
+ filterUsers({});
79
+ // SELECT * FROM users WHERE (TRUE)
80
+ // values: []
81
+ ```
82
+
83
+ ## API
84
+
85
+ Every method is chainable and returns `this`.
86
+
87
+ ### Equality
88
+
89
+ | Method | SQL |
90
+ |---|---|
91
+ | `isEqual(field, value)` | `field = $1` |
92
+ | `isNotEqual(field, value)` | `field != $1` |
93
+
94
+ ### Comparison
95
+
96
+ | Method | SQL |
97
+ |---|---|
98
+ | `isGreater(field, value)` | `field > $1` |
99
+ | `isGreaterOrEqual(field, value)` | `field >= $1` |
100
+ | `isLess(field, value)` | `field < $1` |
101
+ | `isLessOrEqual(field, value)` | `field <= $1` |
102
+ | `isNotGreater(field, value)` | `field <= $1` |
103
+ | `isNotGreaterOrEqual(field, value)` | `field < $1` |
104
+ | `isNotLess(field, value)` | `field >= $1` |
105
+ | `isNotLessOrEqual(field, value)` | `field > $1` |
106
+
107
+ ### Range
108
+
109
+ | Method | SQL |
110
+ |---|---|
111
+ | `isBetween(field, from, to)` | `(field BETWEEN $1 AND $2)` |
112
+ | `isNotBetween(field, from, to)` | `(field NOT BETWEEN $1 AND $2)` |
113
+
114
+ `isBetween` supports partial bounds: if only `from` is provided it becomes `>=`, if only `to` it becomes `<=`. Passing `null` throws an error (use `undefined` to skip a bound).
115
+
116
+ ### Inclusion
117
+
118
+ | Method | SQL |
119
+ |---|---|
120
+ | `isIn(field, values)` | `field IN ($1, $2, ...)` |
121
+ | `isNotIn(field, values)` | `field NOT IN ($1, $2, ...)` |
122
+
123
+ ### Pattern matching
124
+
125
+ | Method | SQL |
126
+ |---|---|
127
+ | `isLike(field, value)` | `field LIKE $1` |
128
+ | `isNotLike(field, value)` | `field NOT LIKE $1` |
129
+ | `isILike(field, value)` | `field ILIKE $1` |
130
+ | `isNotILike(field, value)` | `field NOT ILIKE $1` |
131
+
132
+ `ILIKE` is PostgreSQL-specific (case-insensitive LIKE).
133
+
134
+ ### Null checks
135
+
136
+ | Method | SQL |
137
+ |---|---|
138
+ | `isNull(field, true)` | `field IS NULL` |
139
+ | `isNotNull(field, true)` | `field IS NOT NULL` |
140
+
141
+ The boolean parameter controls whether the condition is added. `isNull('f', false)` is a no-op.
142
+
143
+ ### Raw expressions
144
+
145
+ Use `expression()` to inject raw SQL where a value is expected. No placeholder is generated and no value is added to the parameter array:
146
+
147
+ ```typescript
148
+ const condition = new ConditionBuilder('AND')
149
+ .isEqual('created_at', condition.expression('NOW()'))
150
+ .isGreater('updated_at', condition.expression("NOW() - INTERVAL '1 day'"));
151
+
152
+ condition.build(); // (created_at = NOW() AND updated_at > NOW() - INTERVAL '1 day')
153
+ condition.getValues(); // []
154
+ ```
155
+
156
+ ### Raw conditions
157
+
158
+ Use `raw()` to inject an arbitrary SQL fragment as a condition. Use `?` as placeholder markers -- they will be replaced with the dialect's placeholders and values will be tracked:
159
+
160
+ ```typescript
161
+ const condition = new ConditionBuilder('AND')
162
+ .isEqual('name', 'test')
163
+ .raw('ST_Distance(point, ?) < ?', [somePoint, 100]);
164
+
165
+ condition.build(); // (name = $1 AND ST_Distance(point, $2) < $3)
166
+ condition.getValues(); // ['test', somePoint, 100]
167
+ ```
168
+
169
+ You can also use `raw()` without values for static fragments:
170
+
171
+ ```typescript
172
+ condition.raw('active IS TRUE');
173
+ ```
174
+
175
+ `raw()` follows the same `undefined` philosophy as the rest of the builder. When all values in the array are `undefined`, the condition is skipped. When some are `undefined` and some are not, it throws an error (since partial replacement in arbitrary SQL is ambiguous). Static calls without values (or with an empty array) are always added:
176
+
177
+ ```typescript
178
+ condition.raw('? BETWEEN col1 AND col2', [undefined]); // skipped (all undefined)
179
+ condition.raw('? BETWEEN col1 AND ?', [42, undefined]); // throws Error
180
+ condition.raw('active IS TRUE'); // always added (no values)
181
+ ```
182
+
183
+ Use `\?` to include a literal `?` without it being treated as a placeholder (useful for PostgreSQL's jsonb `?` operator):
184
+
185
+ ```typescript
186
+ condition.raw('data::jsonb \\? ? AND active = ?', ['key', true]);
187
+ // → data::jsonb ? $1 AND active = $2
188
+ ```
189
+
190
+ ### Nesting with `append`
191
+
192
+ Use `append()` to nest a ConditionBuilder inside another, mixing AND/OR logic:
193
+
194
+ ```typescript
195
+ const condition = new ConditionBuilder('AND')
196
+ .isEqual('active', true)
197
+ .append(
198
+ new ConditionBuilder('OR')
199
+ .isEqual('role', 'admin')
200
+ .isEqual('role', 'editor')
201
+ );
202
+
203
+ condition.build(); // (active = $1 AND (role = $2 OR role = $3))
204
+ condition.getValues(); // [true, 'admin', 'editor']
205
+ ```
206
+
207
+ Nesting is recursive -- you can nest as deep as you need.
208
+
209
+ ## Dialects
210
+
211
+ PostgreSQL (`$1, $2, ...`) is the default. You can switch to MySQL (`?`) globally or per instance:
212
+
213
+ ```typescript
214
+ // Global
215
+ ConditionBuilder.DIALECT = 'mysql';
216
+
217
+ // Per instance (overrides global)
218
+ const condition = new ConditionBuilder('AND', 'mysql');
219
+ ```
220
+
221
+ ## Empty conditions
222
+
223
+ An empty AND evaluates to `(TRUE)`, an empty OR to `(FALSE)`. This is safe to include in any query.
@@ -0,0 +1,39 @@
1
+ import type { DialectName } from './types.ts';
2
+ import { Expression } from './Expression.ts';
3
+ import { Condition } from './conditions/Condition.ts';
4
+ export declare class ConditionBuilder extends Condition {
5
+ static DIALECT: DialectName;
6
+ private conditions;
7
+ private mode;
8
+ private dialect;
9
+ constructor(mode: 'AND' | 'OR', dialect?: DialectName);
10
+ expression(value: string): Expression;
11
+ private addComparison;
12
+ isEqual(field: string, value: unknown): this;
13
+ isNotEqual(field: string, value: unknown): this;
14
+ isGreater(field: string, value: unknown): this;
15
+ isNotGreater(field: string, value: unknown): this;
16
+ isGreaterOrEqual(field: string, value: unknown): this;
17
+ isNotGreaterOrEqual(field: string, value: unknown): this;
18
+ isLess(field: string, value: unknown): this;
19
+ isNotLess(field: string, value: unknown): this;
20
+ isLessOrEqual(field: string, value: unknown): this;
21
+ isNotLessOrEqual(field: string, value: unknown): this;
22
+ isLike(field: string, value: unknown): this;
23
+ isNotLike(field: string, value: unknown): this;
24
+ isILike(field: string, value: unknown): this;
25
+ isNotILike(field: string, value: unknown): this;
26
+ raw(sql: string, values?: unknown[]): this;
27
+ private addBetween;
28
+ isBetween(field: string, from: unknown, to: unknown): this;
29
+ isNotBetween(field: string, from: unknown, to: unknown): this;
30
+ isIn(field: string, values: unknown[] | undefined): this;
31
+ isNotIn(field: string, values: unknown[] | undefined): this;
32
+ isNull(field: string, isNull?: boolean): this;
33
+ isNotNull(field: string, isNotNull?: boolean): this;
34
+ append(builder: ConditionBuilder): this;
35
+ build(): string;
36
+ build(startIndex: number, placeholder: (index: number) => string): string;
37
+ getValues(): unknown[];
38
+ }
39
+ //# sourceMappingURL=ConditionBuilder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConditionBuilder.d.ts","sourceRoot":"","sources":["../src/ConditionBuilder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7C,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAQtD,qBAAa,gBAAiB,SAAQ,SAAS;IAC7C,MAAM,CAAC,OAAO,EAAE,WAAW,CAAc;IAEzC,OAAO,CAAC,UAAU,CAAmB;IACrC,OAAO,CAAC,IAAI,CAAe;IAC3B,OAAO,CAAC,OAAO,CAAc;gBAEjB,IAAI,EAAE,KAAK,GAAG,IAAI,EAAE,OAAO,CAAC,EAAE,WAAW;IAMrD,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU;IAIrC,OAAO,CAAC,aAAa;IAMrB,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI;IAM5C,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI;IAM/C,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI;IAI9C,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI;IAIjD,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI;IAIrD,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI;IAIxD,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI;IAI3C,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI;IAI9C,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI;IAIlD,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI;IAIrD,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI;IAI3C,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI;IAI9C,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI;IAI5C,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI;IAI/C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,IAAI;IAY1C,OAAO,CAAC,UAAU;IAiBlB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,GAAG,IAAI;IAI1D,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,GAAG,IAAI;IAI7D,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,SAAS,GAAG,IAAI;IAMxD,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,SAAS,GAAG,IAAI;IAM3D,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI;IAM7C,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,OAAO,GAAG,IAAI;IAMnD,MAAM,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI;IAKvC,KAAK,IAAI,MAAM;IACf,KAAK,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,MAAM;IAkBzE,SAAS,IAAI,OAAO,EAAE;CAGvB"}
@@ -0,0 +1,156 @@
1
+ import { Expression } from "./Expression.js";
2
+ import { getPlaceholder } from "./dialects.js";
3
+ import { Condition } from "./conditions/Condition.js";
4
+ import { EqualCondition } from "./conditions/EqualCondition.js";
5
+ import { ComparisonCondition } from "./conditions/ComparisonCondition.js";
6
+ import { BetweenCondition } from "./conditions/BetweenCondition.js";
7
+ import { InCondition } from "./conditions/InCondition.js";
8
+ import { NullCondition } from "./conditions/NullCondition.js";
9
+ import { RawCondition } from "./conditions/RawCondition.js";
10
+ export class ConditionBuilder extends Condition {
11
+ static DIALECT = 'postgres';
12
+ conditions = [];
13
+ mode;
14
+ dialect;
15
+ constructor(mode, dialect) {
16
+ super();
17
+ this.mode = mode;
18
+ this.dialect = dialect ?? ConditionBuilder.DIALECT;
19
+ }
20
+ expression(value) {
21
+ return new Expression(value);
22
+ }
23
+ addComparison(field, operator, value) {
24
+ if (value === undefined)
25
+ return this;
26
+ this.conditions.push(new ComparisonCondition(field, operator, value));
27
+ return this;
28
+ }
29
+ isEqual(field, value) {
30
+ if (value === undefined)
31
+ return this;
32
+ this.conditions.push(new EqualCondition(field, value));
33
+ return this;
34
+ }
35
+ isNotEqual(field, value) {
36
+ if (value === undefined)
37
+ return this;
38
+ this.conditions.push(new EqualCondition(field, value, true));
39
+ return this;
40
+ }
41
+ isGreater(field, value) {
42
+ return this.addComparison(field, '>', value);
43
+ }
44
+ isNotGreater(field, value) {
45
+ return this.addComparison(field, '<=', value);
46
+ }
47
+ isGreaterOrEqual(field, value) {
48
+ return this.addComparison(field, '>=', value);
49
+ }
50
+ isNotGreaterOrEqual(field, value) {
51
+ return this.addComparison(field, '<', value);
52
+ }
53
+ isLess(field, value) {
54
+ return this.addComparison(field, '<', value);
55
+ }
56
+ isNotLess(field, value) {
57
+ return this.addComparison(field, '>=', value);
58
+ }
59
+ isLessOrEqual(field, value) {
60
+ return this.addComparison(field, '<=', value);
61
+ }
62
+ isNotLessOrEqual(field, value) {
63
+ return this.addComparison(field, '>', value);
64
+ }
65
+ isLike(field, value) {
66
+ return this.addComparison(field, 'LIKE', value);
67
+ }
68
+ isNotLike(field, value) {
69
+ return this.addComparison(field, 'NOT LIKE', value);
70
+ }
71
+ isILike(field, value) {
72
+ return this.addComparison(field, 'ILIKE', value);
73
+ }
74
+ isNotILike(field, value) {
75
+ return this.addComparison(field, 'NOT ILIKE', value);
76
+ }
77
+ raw(sql, values) {
78
+ if (values !== undefined && values.length > 0) {
79
+ const undefinedCount = values.filter(v => v === undefined).length;
80
+ if (undefinedCount === values.length)
81
+ return this;
82
+ if (undefinedCount > 0) {
83
+ throw new Error('raw() does not accept a mix of undefined and defined values');
84
+ }
85
+ }
86
+ this.conditions.push(new RawCondition(sql, values));
87
+ return this;
88
+ }
89
+ addBetween(field, from, to, negated) {
90
+ if (from === null || to === null) {
91
+ throw new Error('isBetween does not accept null values, use undefined to skip a bound');
92
+ }
93
+ if (from === undefined && to === undefined)
94
+ return this;
95
+ if (to === undefined) {
96
+ this.conditions.push(new ComparisonCondition(field, negated ? '<' : '>=', from));
97
+ return this;
98
+ }
99
+ if (from === undefined) {
100
+ this.conditions.push(new ComparisonCondition(field, negated ? '>' : '<=', to));
101
+ return this;
102
+ }
103
+ this.conditions.push(new BetweenCondition(field, from, to, negated));
104
+ return this;
105
+ }
106
+ isBetween(field, from, to) {
107
+ return this.addBetween(field, from, to, false);
108
+ }
109
+ isNotBetween(field, from, to) {
110
+ return this.addBetween(field, from, to, true);
111
+ }
112
+ isIn(field, values) {
113
+ if (values === undefined)
114
+ return this;
115
+ this.conditions.push(new InCondition(field, values));
116
+ return this;
117
+ }
118
+ isNotIn(field, values) {
119
+ if (values === undefined)
120
+ return this;
121
+ this.conditions.push(new InCondition(field, values, true));
122
+ return this;
123
+ }
124
+ isNull(field, isNull) {
125
+ if (!isNull)
126
+ return this;
127
+ this.conditions.push(new NullCondition(field));
128
+ return this;
129
+ }
130
+ isNotNull(field, isNotNull) {
131
+ if (!isNotNull)
132
+ return this;
133
+ this.conditions.push(new NullCondition(field, true));
134
+ return this;
135
+ }
136
+ append(builder) {
137
+ this.conditions.push(builder);
138
+ return this;
139
+ }
140
+ build(startIndex, placeholder) {
141
+ if (this.conditions.length === 0) {
142
+ return this.mode === 'AND' ? '(TRUE)' : '(FALSE)';
143
+ }
144
+ const ph = placeholder ?? getPlaceholder(this.dialect);
145
+ let currentIndex = startIndex ?? 1;
146
+ const parts = [];
147
+ for (const condition of this.conditions) {
148
+ parts.push(condition.build(currentIndex, ph));
149
+ currentIndex += condition.getValues().length;
150
+ }
151
+ return `(${parts.join(` ${this.mode} `)})`;
152
+ }
153
+ getValues() {
154
+ return this.conditions.flatMap(c => c.getValues());
155
+ }
156
+ }
@@ -0,0 +1,5 @@
1
+ export declare class Expression {
2
+ readonly value: string;
3
+ constructor(value: string);
4
+ }
5
+ //# sourceMappingURL=Expression.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Expression.d.ts","sourceRoot":"","sources":["../src/Expression.ts"],"names":[],"mappings":"AAAA,qBAAa,UAAU;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;gBAEX,KAAK,EAAE,MAAM;CAG1B"}
@@ -0,0 +1,6 @@
1
+ export class Expression {
2
+ value;
3
+ constructor(value) {
4
+ this.value = value;
5
+ }
6
+ }
@@ -0,0 +1,11 @@
1
+ import { Condition } from './Condition.ts';
2
+ export declare class BetweenCondition extends Condition {
3
+ private field;
4
+ private from;
5
+ private to;
6
+ private negated;
7
+ constructor(field: string, from: unknown, to: unknown, negated?: boolean);
8
+ build(startIndex: number, placeholder: (index: number) => string): string;
9
+ getValues(): unknown[];
10
+ }
11
+ //# sourceMappingURL=BetweenCondition.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BetweenCondition.d.ts","sourceRoot":"","sources":["../../src/conditions/BetweenCondition.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAG3C,qBAAa,gBAAiB,SAAQ,SAAS;IAC7C,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,IAAI,CAAU;IACtB,OAAO,CAAC,EAAE,CAAU;IACpB,OAAO,CAAC,OAAO,CAAU;gBAEb,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,OAAO,GAAE,OAAe;IAQ/E,KAAK,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,MAAM;IAYzE,SAAS,IAAI,OAAO,EAAE;CAMvB"}
@@ -0,0 +1,34 @@
1
+ import { Condition } from "./Condition.js";
2
+ import { Expression } from "../Expression.js";
3
+ export class BetweenCondition extends Condition {
4
+ field;
5
+ from;
6
+ to;
7
+ negated;
8
+ constructor(field, from, to, negated = false) {
9
+ super();
10
+ this.field = field;
11
+ this.from = from;
12
+ this.to = to;
13
+ this.negated = negated;
14
+ }
15
+ build(startIndex, placeholder) {
16
+ const fromStr = this.from instanceof Expression
17
+ ? this.from.value
18
+ : placeholder(startIndex);
19
+ const toIndex = this.from instanceof Expression ? startIndex : startIndex + 1;
20
+ const toStr = this.to instanceof Expression
21
+ ? this.to.value
22
+ : placeholder(toIndex);
23
+ const op = this.negated ? 'NOT BETWEEN' : 'BETWEEN';
24
+ return `(${this.field} ${op} ${fromStr} AND ${toStr})`;
25
+ }
26
+ getValues() {
27
+ const values = [];
28
+ if (!(this.from instanceof Expression))
29
+ values.push(this.from);
30
+ if (!(this.to instanceof Expression))
31
+ values.push(this.to);
32
+ return values;
33
+ }
34
+ }
@@ -0,0 +1,10 @@
1
+ import { Condition } from './Condition.ts';
2
+ export declare class ComparisonCondition extends Condition {
3
+ private field;
4
+ private operator;
5
+ private value;
6
+ constructor(field: string, operator: string, value: unknown);
7
+ build(startIndex: number, placeholder: (index: number) => string): string;
8
+ getValues(): unknown[];
9
+ }
10
+ //# sourceMappingURL=ComparisonCondition.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ComparisonCondition.d.ts","sourceRoot":"","sources":["../../src/conditions/ComparisonCondition.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAG3C,qBAAa,mBAAoB,SAAQ,SAAS;IAChD,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,KAAK,CAAU;gBAEX,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO;IAO3D,KAAK,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,MAAM;IAOzE,SAAS,IAAI,OAAO,EAAE;CAMvB"}
@@ -0,0 +1,25 @@
1
+ import { Condition } from "./Condition.js";
2
+ import { Expression } from "../Expression.js";
3
+ export class ComparisonCondition extends Condition {
4
+ field;
5
+ operator;
6
+ value;
7
+ constructor(field, operator, value) {
8
+ super();
9
+ this.field = field;
10
+ this.operator = operator;
11
+ this.value = value;
12
+ }
13
+ build(startIndex, placeholder) {
14
+ if (this.value instanceof Expression) {
15
+ return `${this.field} ${this.operator} ${this.value.value}`;
16
+ }
17
+ return `${this.field} ${this.operator} ${placeholder(startIndex)}`;
18
+ }
19
+ getValues() {
20
+ if (this.value instanceof Expression) {
21
+ return [];
22
+ }
23
+ return [this.value];
24
+ }
25
+ }
@@ -0,0 +1,5 @@
1
+ export declare abstract class Condition {
2
+ abstract build(startIndex: number, placeholder: (index: number) => string): string;
3
+ abstract getValues(): unknown[];
4
+ }
5
+ //# sourceMappingURL=Condition.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Condition.d.ts","sourceRoot":"","sources":["../../src/conditions/Condition.ts"],"names":[],"mappings":"AAAA,8BAAsB,SAAS;IAC7B,QAAQ,CAAC,KAAK,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,MAAM;IAClF,QAAQ,CAAC,SAAS,IAAI,OAAO,EAAE;CAChC"}
@@ -0,0 +1,2 @@
1
+ export class Condition {
2
+ }
@@ -0,0 +1,10 @@
1
+ import { Condition } from './Condition.ts';
2
+ export declare class EqualCondition extends Condition {
3
+ private field;
4
+ private value;
5
+ private negated;
6
+ constructor(field: string, value: unknown, negated?: boolean);
7
+ build(startIndex: number, placeholder: (index: number) => string): string;
8
+ getValues(): unknown[];
9
+ }
10
+ //# sourceMappingURL=EqualCondition.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EqualCondition.d.ts","sourceRoot":"","sources":["../../src/conditions/EqualCondition.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAG3C,qBAAa,cAAe,SAAQ,SAAS;IAC3C,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,KAAK,CAAU;IACvB,OAAO,CAAC,OAAO,CAAU;gBAEb,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,GAAE,OAAe;IAOnE,KAAK,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,MAAM;IAQzE,SAAS,IAAI,OAAO,EAAE;CAMvB"}
@@ -0,0 +1,26 @@
1
+ import { Condition } from "./Condition.js";
2
+ import { Expression } from "../Expression.js";
3
+ export class EqualCondition extends Condition {
4
+ field;
5
+ value;
6
+ negated;
7
+ constructor(field, value, negated = false) {
8
+ super();
9
+ this.field = field;
10
+ this.value = value;
11
+ this.negated = negated;
12
+ }
13
+ build(startIndex, placeholder) {
14
+ const op = this.negated ? '!=' : '=';
15
+ if (this.value instanceof Expression) {
16
+ return `${this.field} ${op} ${this.value.value}`;
17
+ }
18
+ return `${this.field} ${op} ${placeholder(startIndex)}`;
19
+ }
20
+ getValues() {
21
+ if (this.value instanceof Expression) {
22
+ return [];
23
+ }
24
+ return [this.value];
25
+ }
26
+ }
@@ -0,0 +1,10 @@
1
+ import { Condition } from './Condition.ts';
2
+ export declare class InCondition extends Condition {
3
+ private field;
4
+ private values;
5
+ private negated;
6
+ constructor(field: string, values: unknown[], negated?: boolean);
7
+ build(startIndex: number, placeholder: (index: number) => string): string;
8
+ getValues(): unknown[];
9
+ }
10
+ //# sourceMappingURL=InCondition.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InCondition.d.ts","sourceRoot":"","sources":["../../src/conditions/InCondition.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAG3C,qBAAa,WAAY,SAAQ,SAAS;IACxC,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,OAAO,CAAU;gBAEb,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,OAAO,GAAE,OAAe;IAOtE,KAAK,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,MAAM;IAezE,SAAS,IAAI,OAAO,EAAE;CAGvB"}
@@ -0,0 +1,31 @@
1
+ import { Condition } from "./Condition.js";
2
+ import { Expression } from "../Expression.js";
3
+ export class InCondition extends Condition {
4
+ field;
5
+ values;
6
+ negated;
7
+ constructor(field, values, negated = false) {
8
+ super();
9
+ this.field = field;
10
+ this.values = values;
11
+ this.negated = negated;
12
+ }
13
+ build(startIndex, placeholder) {
14
+ let currentIndex = startIndex;
15
+ const parts = [];
16
+ for (const value of this.values) {
17
+ if (value instanceof Expression) {
18
+ parts.push(value.value);
19
+ }
20
+ else {
21
+ parts.push(placeholder(currentIndex));
22
+ currentIndex++;
23
+ }
24
+ }
25
+ const op = this.negated ? 'NOT IN' : 'IN';
26
+ return `${this.field} ${op} (${parts.join(', ')})`;
27
+ }
28
+ getValues() {
29
+ return this.values.filter(v => !(v instanceof Expression));
30
+ }
31
+ }
@@ -0,0 +1,9 @@
1
+ import { Condition } from './Condition.ts';
2
+ export declare class NullCondition extends Condition {
3
+ private field;
4
+ private negated;
5
+ constructor(field: string, negated?: boolean);
6
+ build(_startIndex: number, _placeholder: (index: number) => string): string;
7
+ getValues(): unknown[];
8
+ }
9
+ //# sourceMappingURL=NullCondition.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NullCondition.d.ts","sourceRoot":"","sources":["../../src/conditions/NullCondition.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C,qBAAa,aAAc,SAAQ,SAAS;IAC1C,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,OAAO,CAAU;gBAEb,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,OAAe;IAMnD,KAAK,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,MAAM;IAI3E,SAAS,IAAI,OAAO,EAAE;CAGvB"}
@@ -0,0 +1,16 @@
1
+ import { Condition } from "./Condition.js";
2
+ export class NullCondition extends Condition {
3
+ field;
4
+ negated;
5
+ constructor(field, negated = false) {
6
+ super();
7
+ this.field = field;
8
+ this.negated = negated;
9
+ }
10
+ build(_startIndex, _placeholder) {
11
+ return `${this.field} IS ${this.negated ? 'NOT ' : ''}NULL`;
12
+ }
13
+ getValues() {
14
+ return [];
15
+ }
16
+ }
@@ -0,0 +1,9 @@
1
+ import { Condition } from './Condition.ts';
2
+ export declare class RawCondition extends Condition {
3
+ private sql;
4
+ private params;
5
+ constructor(sql: string, params?: unknown[]);
6
+ build(startIndex: number, placeholder: (index: number) => string): string;
7
+ getValues(): unknown[];
8
+ }
9
+ //# sourceMappingURL=RawCondition.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RawCondition.d.ts","sourceRoot":"","sources":["../../src/conditions/RawCondition.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C,qBAAa,YAAa,SAAQ,SAAS;IACzC,OAAO,CAAC,GAAG,CAAS;IACpB,OAAO,CAAC,MAAM,CAAY;gBAEd,GAAG,EAAE,MAAM,EAAE,MAAM,GAAE,OAAO,EAAO;IAM/C,KAAK,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,MAAM;IAQzE,SAAS,IAAI,OAAO,EAAE;CAGvB"}
@@ -0,0 +1,21 @@
1
+ import { Condition } from "./Condition.js";
2
+ export class RawCondition extends Condition {
3
+ sql;
4
+ params;
5
+ constructor(sql, params = []) {
6
+ super();
7
+ this.sql = sql;
8
+ this.params = params;
9
+ }
10
+ build(startIndex, placeholder) {
11
+ let currentIndex = startIndex;
12
+ return this.sql.replace(/\\\?|\?/g, (match) => {
13
+ if (match === '\\?')
14
+ return '?';
15
+ return placeholder(currentIndex++);
16
+ });
17
+ }
18
+ getValues() {
19
+ return this.params;
20
+ }
21
+ }
@@ -0,0 +1,3 @@
1
+ import type { DialectName } from './types.ts';
2
+ export declare function getPlaceholder(dialect: DialectName): (index: number) => string;
3
+ //# sourceMappingURL=dialects.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dialects.d.ts","sourceRoot":"","sources":["../src/dialects.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,wBAAgB,cAAc,CAAC,OAAO,EAAE,WAAW,GAAG,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAO9E"}
@@ -0,0 +1,8 @@
1
+ export function getPlaceholder(dialect) {
2
+ switch (dialect) {
3
+ case 'postgres':
4
+ return (i) => `$${i}`;
5
+ case 'mysql':
6
+ return (_i) => '?';
7
+ }
8
+ }
@@ -0,0 +1,4 @@
1
+ export { ConditionBuilder } from './ConditionBuilder.ts';
2
+ export { Expression } from './Expression.ts';
3
+ export type { DialectName } from './types.ts';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { ConditionBuilder } from "./ConditionBuilder.js";
2
+ export { Expression } from "./Expression.js";
@@ -0,0 +1,2 @@
1
+ export type DialectName = 'postgres' | 'mysql';
2
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,OAAO,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "node-condition-builder",
3
+ "version": "1.0.0",
4
+ "description": "A lightweight TypeScript library for building parameterized SQL WHERE clauses. It generates safe, injection-free condition strings with proper placeholder indexing for PostgreSQL and MySQL",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc -p tsconfig.build.json",
19
+ "prepublishOnly": "npm run lint && npm test && npm run build",
20
+ "test": "node --test 'tests/**/*.test.ts'",
21
+ "lint": "eslint .",
22
+ "lint:fix": "eslint . --fix"
23
+ },
24
+ "keywords": [
25
+ "sql",
26
+ "query-builder",
27
+ "where-clause",
28
+ "condition-builder",
29
+ "parameterized-queries",
30
+ "no-dependencies",
31
+ "postgresql",
32
+ "mysql",
33
+ "typescript"
34
+ ],
35
+ "author": "",
36
+ "license": "ISC",
37
+ "devDependencies": {
38
+ "@types/node": "^25.2.3",
39
+ "eslint": "^9.39.2",
40
+ "jiti": "^2.6.1",
41
+ "typescript": "^5.9.3",
42
+ "typescript-eslint": "^8.55.0"
43
+ }
44
+ }