@maestro-js/sql 1.0.0-alpha.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.
- package/README.md +311 -0
- package/dist/index.d.ts +203 -0
- package/dist/index.js +115 -0
- package/package.json +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
# @maestro-js/sql
|
|
2
|
+
|
|
3
|
+
Composable SQL query builders for constructing parameterized WHERE clauses and Common Table Expressions (CTEs). Standalone utility package with no runtime dependencies on other maestro packages.
|
|
4
|
+
|
|
5
|
+
## Quick Setup
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { Sql } from '@maestro-js/sql'
|
|
9
|
+
|
|
10
|
+
// Dynamic WHERE clause
|
|
11
|
+
const where = Sql.where()
|
|
12
|
+
where.and('status = ?', ['active'])
|
|
13
|
+
where.and('price > ?', [50])
|
|
14
|
+
await Db.select(`SELECT * FROM products WHERE ${where.query}`, where.parameters)
|
|
15
|
+
|
|
16
|
+
// CTE wrapping a Db service
|
|
17
|
+
const withTotals = Sql.cte({
|
|
18
|
+
orderTotals: { subquery: '(SELECT userId, SUM(amount) as total FROM orders GROUP BY userId)', params: [] }
|
|
19
|
+
}, Db)
|
|
20
|
+
await withTotals.select('SELECT * FROM orderTotals WHERE total > ?', [500])
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## WHERE Clause Builder
|
|
24
|
+
|
|
25
|
+
Build parameterized SQL WHERE clauses incrementally with `Sql.where()`. Conditions are appended via `and()` and `or()`. If no conditions are added, `query` returns `'TRUE'`.
|
|
26
|
+
|
|
27
|
+
### Basic Conditions
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
const where = Sql.where()
|
|
31
|
+
where.and('status = ?', ['active'])
|
|
32
|
+
where.and('created_at > ?', ['2024-01-01'])
|
|
33
|
+
// where.query => 'status = ? AND created_at > ?'
|
|
34
|
+
// where.parameters => ['active', '2024-01-01']
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Mixed AND/OR
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
const where = Sql.where()
|
|
41
|
+
where.and('a = ?', [1])
|
|
42
|
+
where.or('b = ?', [2])
|
|
43
|
+
where.and('c = ?', [3])
|
|
44
|
+
// where.query => 'a = ? OR b = ? AND c = ?'
|
|
45
|
+
// where.parameters => [1, 2, 3]
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Nested Groups (Parenthesized Sub-Conditions)
|
|
49
|
+
|
|
50
|
+
Pass a callback to `and()` or `or()` to create parenthesized groups using a sub-builder:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
const where = Sql.where()
|
|
54
|
+
where.and('status = ?', ['active'])
|
|
55
|
+
where.and(sub => {
|
|
56
|
+
sub.or('role = ?', ['admin'])
|
|
57
|
+
sub.or('role = ?', ['editor'])
|
|
58
|
+
})
|
|
59
|
+
// where.query => 'status = ? AND (role = ? OR role = ?)'
|
|
60
|
+
// where.parameters => ['active', 'admin', 'editor']
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Deeply Nested Groups
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
const where = Sql.where()
|
|
67
|
+
where.and('a = ?', [1])
|
|
68
|
+
where.or(qb => {
|
|
69
|
+
qb.and('b = ?', [2])
|
|
70
|
+
qb.or(iqb => {
|
|
71
|
+
iqb.and('c = ?', [3])
|
|
72
|
+
iqb.and('d = ?', [4])
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
// where.query => 'a = ? OR (b = ? OR (c = ? AND d = ?))'
|
|
76
|
+
// where.parameters => [1, 2, 3, 4]
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### IN Clauses
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
const where = Sql.where()
|
|
83
|
+
where.and('id IN (?)', [[1, 2, 3]])
|
|
84
|
+
// where.query => 'id IN (?)'
|
|
85
|
+
// where.parameters => [[1, 2, 3]]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Cloning
|
|
89
|
+
|
|
90
|
+
Pass an existing builder to `Sql.where()` to clone it. The clone is independent of the original:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
const original = Sql.where()
|
|
94
|
+
original.and('a = ?', [1])
|
|
95
|
+
|
|
96
|
+
const clone = Sql.where(original)
|
|
97
|
+
clone.and('b = ?', [2])
|
|
98
|
+
|
|
99
|
+
// original.query => 'a = ?'
|
|
100
|
+
// clone.query => 'a = ? AND b = ?'
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Empty Sub-Builder
|
|
104
|
+
|
|
105
|
+
If a callback adds no conditions, the group is silently skipped:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
const where = Sql.where()
|
|
109
|
+
where.and('a = ?', [1])
|
|
110
|
+
where.and(sub => {
|
|
111
|
+
// no conditions added
|
|
112
|
+
})
|
|
113
|
+
// where.query => 'a = ?'
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## CTE Builder (Common Table Expressions)
|
|
117
|
+
|
|
118
|
+
Wrap a Db service with `Sql.cte()` so every query method auto-prepends the `WITH` clause and merges CTE parameters before the query parameters.
|
|
119
|
+
|
|
120
|
+
### Basic CTE
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
const withStats = Sql.cte({
|
|
124
|
+
userStats: {
|
|
125
|
+
subquery: '(SELECT userId, COUNT(*) as total FROM orders GROUP BY userId)',
|
|
126
|
+
params: []
|
|
127
|
+
}
|
|
128
|
+
}, Db)
|
|
129
|
+
|
|
130
|
+
const results = await withStats.select('SELECT * FROM userStats WHERE total > ?', [10])
|
|
131
|
+
// Executes: WITH userStats AS (SELECT userId, COUNT(*) ...) SELECT * FROM userStats WHERE total > ?
|
|
132
|
+
// Parameters: [10]
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Multiple CTEs
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
const builder = Sql.cte({
|
|
139
|
+
activeUsers: {
|
|
140
|
+
subquery: '(SELECT * FROM users WHERE active = ?)',
|
|
141
|
+
params: [true]
|
|
142
|
+
},
|
|
143
|
+
recentOrders: {
|
|
144
|
+
subquery: '(SELECT * FROM orders WHERE created_at > ?)',
|
|
145
|
+
params: ['2024-01-01']
|
|
146
|
+
}
|
|
147
|
+
}, Db)
|
|
148
|
+
|
|
149
|
+
await builder.select('SELECT * FROM activeUsers JOIN recentOrders ON activeUsers.id = recentOrders.userId')
|
|
150
|
+
// WITH activeUsers AS (...), recentOrders AS (...) SELECT ...
|
|
151
|
+
// Parameters: [true, '2024-01-01']
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Recursive CTEs
|
|
155
|
+
|
|
156
|
+
Set `recursive: true` on any expression to trigger `WITH RECURSIVE`:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
const builder = Sql.cte({
|
|
160
|
+
hierarchy: {
|
|
161
|
+
subquery: '(SELECT id, parentId, name FROM categories WHERE parentId IS NULL UNION ALL SELECT c.id, c.parentId, c.name FROM categories c JOIN hierarchy h ON c.parentId = h.id)',
|
|
162
|
+
params: [],
|
|
163
|
+
recursive: true
|
|
164
|
+
}
|
|
165
|
+
}, Db)
|
|
166
|
+
// WITH RECURSIVE hierarchy AS (...)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Dynamic Parameters
|
|
170
|
+
|
|
171
|
+
Pass a function for `params` to resolve values at query time:
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
const builder = Sql.cte({
|
|
175
|
+
filtered: {
|
|
176
|
+
subquery: '(SELECT * FROM items WHERE tenant_id = ?)',
|
|
177
|
+
params: () => [getCurrentTenantId()]
|
|
178
|
+
}
|
|
179
|
+
}, Db)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Re-Wrapping with fromDbService
|
|
183
|
+
|
|
184
|
+
Use `fromDbService()` to apply the same CTEs to a different Db service (e.g., inside a transaction):
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
const cteBuilder = Sql.cte({ ... }, Db)
|
|
188
|
+
const txResult = await Db.transaction(async () => {
|
|
189
|
+
const txCte = cteBuilder.fromDbService(TxDb)
|
|
190
|
+
return txCte.select('SELECT * FROM ...')
|
|
191
|
+
})
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Wrapped Methods
|
|
195
|
+
|
|
196
|
+
The CTE builder exposes all standard Db methods: `select`, `selectIterable`, `insert`, `update`, `delete`, `statement`, `scalar`, `format`, `transaction`.
|
|
197
|
+
|
|
198
|
+
## API Reference
|
|
199
|
+
|
|
200
|
+
### Sql.where(existing?)
|
|
201
|
+
|
|
202
|
+
Create a new WhereClauseBuilder. Pass an existing builder to clone it.
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
function WhereClauseBuilder(existing?: WhereClauseBuilder): WhereClauseBuilder
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**WhereClauseBuilder interface:**
|
|
209
|
+
|
|
210
|
+
| Member | Type | Description |
|
|
211
|
+
|--------------|--------------------------------------------------------|----------------------------------------------------------|
|
|
212
|
+
| `and()` | `(condition: string \| Callback, params?: any[]) => void` | Append condition with AND, or parenthesized group via callback |
|
|
213
|
+
| `or()` | `(condition: string \| Callback, params?: any[]) => void` | Append condition with OR, or parenthesized group via callback |
|
|
214
|
+
| `query` | `string` (readonly getter) | Assembled clause string, or `'TRUE'` if empty |
|
|
215
|
+
| `parameters` | `any[]` (readonly getter) | Parameter array matching `?` placeholders in query |
|
|
216
|
+
|
|
217
|
+
### Sql.cte(expressions, db)
|
|
218
|
+
|
|
219
|
+
Create a CTE builder wrapping a Db service.
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
function CommonTableExpressionBuilder(
|
|
223
|
+
commonTableExpressions: CommonTableExpressionSet,
|
|
224
|
+
db: CteDbService
|
|
225
|
+
): CteDbService & { fromDbService(db: CteDbService): CteDbService }
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
**CommonTableExpressionSet:**
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
interface CommonTableExpressionSet {
|
|
232
|
+
[name: string]: {
|
|
233
|
+
subquery: string // CTE body wrapped in parentheses
|
|
234
|
+
params?: any[] | (() => any[]) // Static array or dynamic resolver
|
|
235
|
+
recursive?: boolean // Triggers WITH RECURSIVE
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Type Exports
|
|
241
|
+
|
|
242
|
+
Access types through the `Sql` namespace:
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
Sql.cte.Expression // CommonTableExpressionConfig
|
|
246
|
+
Sql.cte.ExpressionSet // CommonTableExpressionSet
|
|
247
|
+
Sql.cte.DbService // CteDbService
|
|
248
|
+
Sql.where.Builder // WhereClauseBuilder
|
|
249
|
+
Sql.where.Callback // WhereClauseCallback
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
`Sql.commonTableExpression` is an alias for `Sql.cte` (both the function and its namespace types).
|
|
253
|
+
|
|
254
|
+
## Important: Avoid Table Name Aliases
|
|
255
|
+
|
|
256
|
+
When writing SQL queries with these builders, avoid table name aliases when possible. Write full table names for clarity:
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
// Preferred
|
|
260
|
+
where.and('users.status = ?', ['active'])
|
|
261
|
+
await builder.select('SELECT users.id, orders.total FROM users JOIN orders ON users.id = orders.userId')
|
|
262
|
+
|
|
263
|
+
// Avoid
|
|
264
|
+
where.and('u.status = ?', ['active'])
|
|
265
|
+
await builder.select('SELECT u.id, o.total FROM users u JOIN orders o ON u.id = o.userId')
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## Testing
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
# Run tests
|
|
272
|
+
pnpm --filter @maestro-js/sql test
|
|
273
|
+
|
|
274
|
+
# Single test file
|
|
275
|
+
cd packages/sql && npx beartest ./tests/where-clause.test.ts
|
|
276
|
+
|
|
277
|
+
# Typecheck
|
|
278
|
+
pnpm --filter @maestro-js/sql typecheck
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Tests use `beartest-js` with `expect` for assertions. Test file location: `packages/sql/tests/where-clause.test.ts`.
|
|
282
|
+
|
|
283
|
+
## Cross-Package Integration
|
|
284
|
+
|
|
285
|
+
- **No runtime dependencies** on other maestro packages
|
|
286
|
+
- **Works with @maestro-js/db**: SQL builders produce `{ query, parameters }` pairs for Db to execute. The CTE builder wraps a Db service (or any object matching `CteDbService`) to auto-prepend WITH clauses.
|
|
287
|
+
- **@maestro-js/db is a devDependency** only, used for the `CteDbService` type interface
|
|
288
|
+
|
|
289
|
+
### Typical Usage with Db
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
import { Sql } from '@maestro-js/sql'
|
|
293
|
+
import { Db } from '@maestro-js/db'
|
|
294
|
+
|
|
295
|
+
// WHERE builder feeds into Db.select
|
|
296
|
+
const where = Sql.where()
|
|
297
|
+
where.and('active = ?', [true])
|
|
298
|
+
if (minPrice) where.and('price >= ?', [minPrice])
|
|
299
|
+
if (category) where.and('category = ?', [category])
|
|
300
|
+
|
|
301
|
+
const products = await Db.select(`SELECT * FROM products WHERE ${where.query}`, where.parameters)
|
|
302
|
+
|
|
303
|
+
// CTE builder wraps Db directly
|
|
304
|
+
const withMetrics = Sql.cte({
|
|
305
|
+
dailySales: {
|
|
306
|
+
subquery: '(SELECT DATE(created_at) as day, SUM(amount) as total FROM orders GROUP BY DATE(created_at))',
|
|
307
|
+
params: []
|
|
308
|
+
}
|
|
309
|
+
}, Db)
|
|
310
|
+
const trends = await withMetrics.select('SELECT * FROM dailySales WHERE total > ?', [1000])
|
|
311
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/** Map of CTE names to their subquery, parameters, and recursive flag */
|
|
2
|
+
interface CommonTableExpressionSet {
|
|
3
|
+
[name: string]: {
|
|
4
|
+
/** The CTE body, wrapped in parentheses (e.g. `'(SELECT * FROM users WHERE active = ?)'`) */
|
|
5
|
+
subquery: string;
|
|
6
|
+
/** Parameters for the subquery placeholders — pass a function for dynamic resolution */
|
|
7
|
+
params?: any[] | (() => any[]);
|
|
8
|
+
/** Marks this CTE as recursive — triggers `WITH RECURSIVE` for the entire statement */
|
|
9
|
+
recursive?: boolean;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
/** Merged configuration for a single CTE after parameter resolution */
|
|
13
|
+
interface CommonTableExpressionConfig {
|
|
14
|
+
subquery: string;
|
|
15
|
+
params?: any[];
|
|
16
|
+
recursive?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Db service interface that CTE builders accept and return.
|
|
20
|
+
*
|
|
21
|
+
* Mirrors the core Db service methods. The CTE builder wraps each method to
|
|
22
|
+
* automatically prepend the `WITH` clause and merge CTE parameters before
|
|
23
|
+
* the query parameters.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* const withTotals = Sql.cte({
|
|
28
|
+
* orderTotals: { subquery: '(SELECT userId, SUM(amount) as total FROM orders GROUP BY userId)', params: [] }
|
|
29
|
+
* }, Db)
|
|
30
|
+
* await withTotals.select('SELECT * FROM orderTotals WHERE total > ?', [500])
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
interface CteDbService {
|
|
34
|
+
/** Executes a SELECT query, returning an array of typed rows */
|
|
35
|
+
select<T = Record<string, unknown>>(query: string, parameters?: any[]): Promise<T[]>;
|
|
36
|
+
/** Executes a SELECT query, returning an async iterable for streaming results */
|
|
37
|
+
selectIterable<T = Record<string, unknown>>(query: string, parameters?: any[]): any;
|
|
38
|
+
/** Executes an INSERT query, returning affected row count and insert ID */
|
|
39
|
+
insert(query: string, parameters?: any[]): Promise<{
|
|
40
|
+
affectedRows: number;
|
|
41
|
+
insertId: number;
|
|
42
|
+
}>;
|
|
43
|
+
/** Executes an UPDATE query, returning affected and changed row counts */
|
|
44
|
+
update(query: string, parameters?: any[]): Promise<{
|
|
45
|
+
affectedRows: number;
|
|
46
|
+
changedRows: number;
|
|
47
|
+
}>;
|
|
48
|
+
/** Executes a DELETE query, returning affected row count */
|
|
49
|
+
delete(query: string, parameters?: any[]): Promise<{
|
|
50
|
+
affectedRows: number;
|
|
51
|
+
}>;
|
|
52
|
+
/** Executes an arbitrary SQL statement */
|
|
53
|
+
statement<T = unknown>(query: string, parameters?: any[]): Promise<T>;
|
|
54
|
+
/** Executes a query and returns a single scalar value, or `null` */
|
|
55
|
+
scalar<T = unknown>(query: string, parameters?: any[]): Promise<T | null>;
|
|
56
|
+
/** Executes the callback within a database transaction */
|
|
57
|
+
transaction: <T>(callback: () => Promise<T>) => Promise<T>;
|
|
58
|
+
/** Formats a query with interpolated parameters for debugging */
|
|
59
|
+
format: (query: string, params?: any[]) => string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Creates a CTE builder that wraps a Db service with Common Table Expressions.
|
|
63
|
+
*
|
|
64
|
+
* Every query method on the returned object automatically prepends the `WITH`
|
|
65
|
+
* clause and merges CTE parameters before the query parameters. Use
|
|
66
|
+
* `fromDbService()` to re-wrap a different Db service with the same CTEs.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```ts
|
|
70
|
+
* const withStats = Sql.cte({
|
|
71
|
+
* userStats: { subquery: '(SELECT userId, COUNT(*) as total FROM orders GROUP BY userId)', params: [] }
|
|
72
|
+
* }, Db)
|
|
73
|
+
* const results = await withStats.select('SELECT * FROM userStats WHERE total > ?', [10])
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
declare function CommonTableExpressionBuilder(commonTableExpressions: CommonTableExpressionSet, db: CteDbService): {
|
|
77
|
+
fromDbService: (db: CteDbService) => CteDbService;
|
|
78
|
+
select: <T = Record<string, unknown>>(query: string, parameters?: any[]) => Promise<T[]>;
|
|
79
|
+
selectIterable: <T = Record<string, unknown>>(query: string, parameters?: any[]) => any;
|
|
80
|
+
insert: (query: string, parameters?: any[]) => Promise<{
|
|
81
|
+
affectedRows: number;
|
|
82
|
+
insertId: number;
|
|
83
|
+
}>;
|
|
84
|
+
update: (query: string, parameters?: any[]) => Promise<{
|
|
85
|
+
affectedRows: number;
|
|
86
|
+
changedRows: number;
|
|
87
|
+
}>;
|
|
88
|
+
delete: (query: string, parameters?: any[]) => Promise<{
|
|
89
|
+
affectedRows: number;
|
|
90
|
+
}>;
|
|
91
|
+
statement: <T = unknown>(query: string, parameters?: any[]) => Promise<T>;
|
|
92
|
+
scalar: <T = unknown>(query: string, parameters?: any[]) => Promise<T | null>;
|
|
93
|
+
format: (query: string, params?: any[]) => string;
|
|
94
|
+
transaction: <T>(callback: () => Promise<T>) => Promise<T>;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/** Callback that receives a sub-builder for creating parenthesized condition groups */
|
|
98
|
+
type WhereClauseCallback = (builder: WhereClauseBuilder) => void;
|
|
99
|
+
/**
|
|
100
|
+
* Mutable builder for constructing parameterized SQL WHERE clauses.
|
|
101
|
+
*
|
|
102
|
+
* Conditions are appended incrementally with {@link and} and {@link or}. Pass a
|
|
103
|
+
* callback to either method to wrap conditions in parentheses. Read {@link query}
|
|
104
|
+
* and {@link parameters} to get the assembled clause and its matching parameter array.
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```ts
|
|
108
|
+
* const where = Sql.where()
|
|
109
|
+
* where.and('status = ?', ['active'])
|
|
110
|
+
* where.and(sub => {
|
|
111
|
+
* sub.or('role = ?', ['admin'])
|
|
112
|
+
* sub.or('role = ?', ['editor'])
|
|
113
|
+
* })
|
|
114
|
+
* where.query // 'status = ? AND (role = ? OR role = ?)'
|
|
115
|
+
* where.parameters // ['active', 'admin', 'editor']
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
interface WhereClauseBuilder {
|
|
119
|
+
/** Appends a condition joined by `AND`, or a parenthesized group when given a callback */
|
|
120
|
+
and(condition: string | WhereClauseCallback, params?: any[]): void;
|
|
121
|
+
/** Appends a condition joined by `OR`, or a parenthesized group when given a callback */
|
|
122
|
+
or(condition: string | WhereClauseCallback, params?: any[]): void;
|
|
123
|
+
/** Returns the assembled WHERE clause string, or `'TRUE'` if no conditions were added */
|
|
124
|
+
readonly query: string;
|
|
125
|
+
/** Returns the parameter array matching the `?` placeholders in {@link query} */
|
|
126
|
+
readonly parameters: any[];
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Creates a new WHERE clause builder for constructing parameterized SQL conditions.
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* ```ts
|
|
133
|
+
* const builder = WhereClauseBuilder()
|
|
134
|
+
* builder.and('a = ?', [1])
|
|
135
|
+
* builder.and('b IN (?)', [[2, 3]])
|
|
136
|
+
* builder.or(qb => {
|
|
137
|
+
* qb.and('c = ?', [4])
|
|
138
|
+
* qb.or('d = ?', [5])
|
|
139
|
+
* })
|
|
140
|
+
* // a = ? AND b IN (?) OR (c = ? OR d = ?)
|
|
141
|
+
* // params: [1, [2, 3], 4, 5]
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
declare function WhereClauseBuilder(existing?: WhereClauseBuilder): WhereClauseBuilder;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Composable SQL query builders for dynamic WHERE clauses and Common Table Expressions.
|
|
148
|
+
* Use `Sql.where()` for parameterized filters and `Sql.cte()` to wrap a Db service with CTEs.
|
|
149
|
+
*/
|
|
150
|
+
declare const Sql: {
|
|
151
|
+
cte: typeof CommonTableExpressionBuilder;
|
|
152
|
+
commonTableExpression: typeof CommonTableExpressionBuilder;
|
|
153
|
+
where: typeof WhereClauseBuilder;
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* Composable SQL query builders for dynamic WHERE clauses and Common Table Expressions.
|
|
157
|
+
*
|
|
158
|
+
* The WHERE builder produces `{ query, parameters }` pairs for safe, incremental filter
|
|
159
|
+
* construction. The CTE builder wraps a Db service so every query method automatically
|
|
160
|
+
* prepends `WITH` expressions and merges parameters.
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* ```ts
|
|
164
|
+
* // Dynamic WHERE clause
|
|
165
|
+
* const where = Sql.where()
|
|
166
|
+
* where.and('status = ?', ['active'])
|
|
167
|
+
* where.and('price > ?', [50])
|
|
168
|
+
* await Db.select(`SELECT * FROM products WHERE ${where.query}`, where.parameters)
|
|
169
|
+
*
|
|
170
|
+
* // CTE wrapping a Db service
|
|
171
|
+
* const withTotals = Sql.cte({
|
|
172
|
+
* orderTotals: { subquery: '(SELECT userId, SUM(amount) as total FROM orders GROUP BY userId)', params: [] }
|
|
173
|
+
* }, Db)
|
|
174
|
+
* await withTotals.select('SELECT * FROM orderTotals WHERE total > ?', [500])
|
|
175
|
+
* ```
|
|
176
|
+
*/
|
|
177
|
+
declare namespace Sql {
|
|
178
|
+
namespace cte {
|
|
179
|
+
/** Configuration for a single CTE — subquery, parameters, and optional recursive flag */
|
|
180
|
+
type Expression = CommonTableExpressionConfig;
|
|
181
|
+
/** Map of CTE names to their configurations */
|
|
182
|
+
type ExpressionSet = CommonTableExpressionSet;
|
|
183
|
+
/** Db service interface that CTE builders accept and return */
|
|
184
|
+
type DbService = CteDbService;
|
|
185
|
+
}
|
|
186
|
+
/** Alias for {@link cte} — same types under the full name `commonTableExpression` */
|
|
187
|
+
namespace commonTableExpression {
|
|
188
|
+
/** Configuration for a single CTE — subquery, parameters, and optional recursive flag */
|
|
189
|
+
type Expression = CommonTableExpressionConfig;
|
|
190
|
+
/** Map of CTE names to their configurations */
|
|
191
|
+
type ExpressionSet = CommonTableExpressionSet;
|
|
192
|
+
/** Db service interface that CTE builders accept and return */
|
|
193
|
+
type DbService = CteDbService;
|
|
194
|
+
}
|
|
195
|
+
namespace where {
|
|
196
|
+
/** Mutable builder with `and()`, `or()`, `query`, and `parameters` for constructing WHERE clauses */
|
|
197
|
+
type Builder = WhereClauseBuilder;
|
|
198
|
+
/** Callback that receives a sub-builder for creating parenthesized condition groups */
|
|
199
|
+
type Callback = WhereClauseCallback;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export { Sql };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// src/cte.ts
|
|
2
|
+
function mergeExpressions(commonTableExpressions) {
|
|
3
|
+
const queries = Object.entries(commonTableExpressions).map(([name, cte]) => {
|
|
4
|
+
const params2 = typeof cte.params === "function" ? cte.params() : cte.params;
|
|
5
|
+
return { statement: `${name} AS ${cte.subquery}`, params: params2 ?? [], recursive: cte.recursive };
|
|
6
|
+
});
|
|
7
|
+
const params = queries.flatMap((q) => q.params);
|
|
8
|
+
const subquery = `WITH ${queries.some((q) => q.recursive) ? "RECURSIVE " : ""}${queries.map((q) => q.statement).join(", ")}`;
|
|
9
|
+
return { params, subquery };
|
|
10
|
+
}
|
|
11
|
+
function CommonTableExpressionBuilder(commonTableExpressions, db) {
|
|
12
|
+
const cte = mergeExpressions(commonTableExpressions);
|
|
13
|
+
function getQueryAndParams(query, params = []) {
|
|
14
|
+
const q = `${cte.subquery} ${query}`;
|
|
15
|
+
const p = [...cte.params ?? [], ...params];
|
|
16
|
+
return [q, p];
|
|
17
|
+
}
|
|
18
|
+
function fromDbService(db2) {
|
|
19
|
+
return {
|
|
20
|
+
transaction: db2.transaction,
|
|
21
|
+
format(query, params) {
|
|
22
|
+
return db2.format(...getQueryAndParams(query, params));
|
|
23
|
+
},
|
|
24
|
+
select(query, params) {
|
|
25
|
+
return db2.select(...getQueryAndParams(query, params));
|
|
26
|
+
},
|
|
27
|
+
selectIterable(query, params) {
|
|
28
|
+
return db2.selectIterable(...getQueryAndParams(query, params));
|
|
29
|
+
},
|
|
30
|
+
insert(query, params) {
|
|
31
|
+
return db2.insert(...getQueryAndParams(query, params));
|
|
32
|
+
},
|
|
33
|
+
update(query, params) {
|
|
34
|
+
return db2.update(...getQueryAndParams(query, params));
|
|
35
|
+
},
|
|
36
|
+
delete(query, params) {
|
|
37
|
+
return db2.delete(...getQueryAndParams(query, params));
|
|
38
|
+
},
|
|
39
|
+
statement(query, params) {
|
|
40
|
+
return db2.statement(...getQueryAndParams(query, params));
|
|
41
|
+
},
|
|
42
|
+
scalar(query, params) {
|
|
43
|
+
return db2.scalar(...getQueryAndParams(query, params));
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const facade = fromDbService(db);
|
|
48
|
+
return {
|
|
49
|
+
fromDbService,
|
|
50
|
+
select: facade.select,
|
|
51
|
+
selectIterable: facade.selectIterable,
|
|
52
|
+
insert: facade.insert,
|
|
53
|
+
update: facade.update,
|
|
54
|
+
delete: facade.delete,
|
|
55
|
+
statement: facade.statement,
|
|
56
|
+
scalar: facade.scalar,
|
|
57
|
+
format: facade.format,
|
|
58
|
+
transaction: facade.transaction
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/where-clause.ts
|
|
63
|
+
function WhereClauseBuilder(existing) {
|
|
64
|
+
let clause = existing?.query === "TRUE" ? void 0 : existing?.query;
|
|
65
|
+
const params = [...existing?.parameters || []];
|
|
66
|
+
function add(glue, condition, newParams = []) {
|
|
67
|
+
let conditionClause;
|
|
68
|
+
let conditionParams;
|
|
69
|
+
if (typeof condition === "function") {
|
|
70
|
+
const subBuilder = WhereClauseBuilder();
|
|
71
|
+
condition(subBuilder);
|
|
72
|
+
const subQuery = subBuilder.query;
|
|
73
|
+
if (subQuery === "TRUE") {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
conditionClause = `(${subQuery})`;
|
|
77
|
+
conditionParams = subBuilder.parameters;
|
|
78
|
+
} else {
|
|
79
|
+
conditionClause = condition;
|
|
80
|
+
conditionParams = newParams;
|
|
81
|
+
}
|
|
82
|
+
if (!clause) {
|
|
83
|
+
clause = conditionClause;
|
|
84
|
+
} else {
|
|
85
|
+
clause = `${clause} ${glue} ${conditionClause}`;
|
|
86
|
+
}
|
|
87
|
+
params.push(...conditionParams);
|
|
88
|
+
}
|
|
89
|
+
function and(condition, params2 = []) {
|
|
90
|
+
add("AND", condition, params2);
|
|
91
|
+
}
|
|
92
|
+
function or(condition, params2 = []) {
|
|
93
|
+
add("OR", condition, params2);
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
and,
|
|
97
|
+
or,
|
|
98
|
+
get query() {
|
|
99
|
+
return clause || "TRUE";
|
|
100
|
+
},
|
|
101
|
+
get parameters() {
|
|
102
|
+
return params.length > 0 ? params : [];
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/index.ts
|
|
108
|
+
var Sql = {
|
|
109
|
+
cte: CommonTableExpressionBuilder,
|
|
110
|
+
commonTableExpression: CommonTableExpressionBuilder,
|
|
111
|
+
where: WhereClauseBuilder
|
|
112
|
+
};
|
|
113
|
+
export {
|
|
114
|
+
Sql
|
|
115
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@maestro-js/sql",
|
|
3
|
+
"description": "Composable SQL query builders for dynamic WHERE clauses and Common Table Expressions (CTEs). Use when working with @maestro-js/sql, building dynamic SQL filters, constructing parameterized WHERE clauses, wrapping Db services with CTEs, or generating safe parameterized SQL fragments. Key capabilities: WhereClauseBuilder for incremental AND/OR conditions with nested sub-groups, CommonTableExpressionBuilder for wrapping a Db service so all queries auto-prepend WITH clauses, cloning builders, recursive CTEs, and dynamic parameter resolution.",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"default": "./dist/index.js"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@maestro-js/db": "1.0.0-alpha.0"
|
|
13
|
+
},
|
|
14
|
+
"version": "1.0.0-alpha.0",
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "restricted"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"license": "UNLICENSED",
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=22.18.0"
|
|
24
|
+
},
|
|
25
|
+
"repository": "https://github.com/Marcato-Partners/maestro-js",
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsup --config ../../tsup.config.ts",
|
|
28
|
+
"typecheck": "tsc --noEmit",
|
|
29
|
+
"test": "beartest ./tests/**/*",
|
|
30
|
+
"format": "prettier --write src/ tests/",
|
|
31
|
+
"lint": "prettier --check src/ tests/"
|
|
32
|
+
}
|
|
33
|
+
}
|