@wowsql/sdk 3.5.0 → 3.6.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 CHANGED
@@ -312,6 +312,138 @@ console.log(result.affected_rows); // Number of rows deleted
312
312
 
313
313
  // Is null
314
314
  .filter({ column: 'deleted_at', operator: 'is', value: null })
315
+
316
+ // IN operator (value must be an array)
317
+ .filter('category', 'in', ['electronics', 'books', 'clothing'])
318
+
319
+ // NOT IN operator
320
+ .filter('status', 'not_in', ['deleted', 'archived'])
321
+
322
+ // BETWEEN operator (value must be an array of 2 values)
323
+ .filter('price', 'between', [10, 100])
324
+
325
+ // NOT BETWEEN operator
326
+ .filter('age', 'not_between', [18, 65])
327
+
328
+ // OR logical operator
329
+ .filter('category', 'eq', 'electronics', 'AND')
330
+ .filter('price', 'gt', 1000, 'OR') // OR condition
331
+ ```
332
+
333
+ ## Advanced Query Features
334
+
335
+ ### GROUP BY and Aggregates
336
+
337
+ GROUP BY supports both simple column names and SQL expressions with functions. All expressions are validated for security.
338
+
339
+ #### Basic GROUP BY
340
+
341
+ ```typescript
342
+ // Group by single column
343
+ const result = await client.table("products")
344
+ .select("category", "COUNT(*) as count", "AVG(price) as avg_price")
345
+ .groupBy("category")
346
+ .get();
347
+
348
+ // Group by multiple columns
349
+ const result = await client.table("sales")
350
+ .select("region", "category", "SUM(amount) as total")
351
+ .groupBy(["region", "category"])
352
+ .get();
353
+ ```
354
+
355
+ #### GROUP BY with Date/Time Functions
356
+
357
+ ```typescript
358
+ // Group by date
359
+ const result = await client.table("orders")
360
+ .select("DATE(created_at) as date", "COUNT(*) as orders", "SUM(total) as revenue")
361
+ .groupBy("DATE(created_at)")
362
+ .orderBy("date", "desc")
363
+ .get();
364
+
365
+ // Group by year and month
366
+ const result = await client.table("orders")
367
+ .select("YEAR(created_at) as year", "MONTH(created_at) as month", "SUM(total) as revenue")
368
+ .groupBy(["YEAR(created_at)", "MONTH(created_at)"])
369
+ .get();
370
+
371
+ // Group by week
372
+ const result = await client.table("orders")
373
+ .select("WEEK(created_at) as week", "COUNT(*) as orders")
374
+ .groupBy("WEEK(created_at)")
375
+ .get();
376
+ ```
377
+
378
+ #### Supported Functions in GROUP BY
379
+
380
+ **Date/Time:** `DATE()`, `YEAR()`, `MONTH()`, `DAY()`, `WEEK()`, `QUARTER()`, `HOUR()`, `MINUTE()`, `SECOND()`, `DATE_FORMAT()`, `DATE_ADD()`, `DATE_SUB()`, `DATEDIFF()`, `NOW()`, `CURRENT_TIMESTAMP()`, etc.
381
+
382
+ **String:** `CONCAT()`, `SUBSTRING()`, `LEFT()`, `RIGHT()`, `UPPER()`, `LOWER()`, `LENGTH()`, `TRIM()`, etc.
383
+
384
+ **Mathematical:** `ROUND()`, `CEIL()`, `FLOOR()`, `ABS()`, `POW()`, `SQRT()`, `MOD()`, etc.
385
+
386
+ ### HAVING Clause
387
+
388
+ HAVING filters aggregated results after GROUP BY. Supports aggregate functions and comparison operators.
389
+
390
+ ```typescript
391
+ // Filter aggregated results
392
+ const result = await client.table("products")
393
+ .select("category", "COUNT(*) as count")
394
+ .groupBy("category")
395
+ .having("COUNT(*)", "gt", 10)
396
+ .get();
397
+
398
+ // Multiple HAVING conditions
399
+ const result = await client.table("orders")
400
+ .select("DATE(created_at) as date", "SUM(total) as revenue")
401
+ .groupBy("DATE(created_at)")
402
+ .having("SUM(total)", "gt", 1000)
403
+ .having("COUNT(*)", "gte", 5)
404
+ .get();
405
+
406
+ // HAVING with different aggregate functions
407
+ const result = await client.table("products")
408
+ .select("category", "AVG(price) as avg_price", "COUNT(*) as count")
409
+ .groupBy("category")
410
+ .having("AVG(price)", "gt", 100)
411
+ .having("COUNT(*)", "gte", 5)
412
+ .get();
413
+ ```
414
+
415
+ **Supported HAVING Operators:** `eq`, `neq`, `gt`, `gte`, `lt`, `lte`
416
+
417
+ **Supported Aggregate Functions:** `COUNT(*)`, `SUM()`, `AVG()`, `MAX()`, `MIN()`, `GROUP_CONCAT()`, `STDDEV()`, `VARIANCE()`
418
+
419
+ ### Multiple ORDER BY
420
+
421
+ ```typescript
422
+ // Order by multiple columns
423
+ const result = await client.table("products")
424
+ .select("*")
425
+ .orderByMultiple([
426
+ { column: "category", direction: "asc" },
427
+ { column: "price", direction: "desc" },
428
+ { column: "created_at", direction: "desc" }
429
+ ])
430
+ .get();
431
+ ```
432
+
433
+ ### Date/Time Functions
434
+
435
+ ```typescript
436
+ // Filter by date range using functions
437
+ const result = await client.table("orders")
438
+ .select("*")
439
+ .filter("created_at", "gte", "DATE_SUB(NOW(), INTERVAL 7 DAY)")
440
+ .get();
441
+
442
+ // Group by date
443
+ const result = await client.table("orders")
444
+ .select("DATE(created_at) as date", "COUNT(*) as count")
445
+ .groupBy("DATE(created_at)")
446
+ .get();
315
447
  ```
316
448
 
317
449
  ### Complex Queries
package/dist/index.d.ts CHANGED
@@ -20,13 +20,17 @@ export interface WowSQLConfig {
20
20
  timeout?: number;
21
21
  }
22
22
  export interface QueryOptions {
23
- /** Columns to select (comma-separated or array) */
23
+ /** Columns to select (comma-separated or array) - can include expressions like "COUNT(*)", "DATE(created_at) as date" */
24
24
  select?: string | string[];
25
25
  /** Filter expressions */
26
26
  filter?: FilterExpression | FilterExpression[];
27
- /** Column to sort by */
28
- order?: string;
29
- /** Sort direction */
27
+ /** Columns to group by */
28
+ group_by?: string | string[];
29
+ /** HAVING clause filters for aggregated results */
30
+ having?: HavingFilter | HavingFilter[];
31
+ /** Column(s) to sort by - can be string or array of OrderByItem */
32
+ order?: string | OrderByItem[];
33
+ /** Sort direction (used only if order is a string) */
30
34
  orderDirection?: 'asc' | 'desc';
31
35
  /** Maximum records to return */
32
36
  limit?: number;
@@ -35,9 +39,19 @@ export interface QueryOptions {
35
39
  }
36
40
  export interface FilterExpression {
37
41
  column: string;
38
- operator: 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'like' | 'is';
42
+ operator: 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'like' | 'is' | 'in' | 'not_in' | 'between' | 'not_between' | 'is_not';
43
+ value: string | number | boolean | null | any[] | [any, any];
44
+ logical_op?: 'AND' | 'OR';
45
+ }
46
+ export interface HavingFilter {
47
+ column: string;
48
+ operator: 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte';
39
49
  value: string | number | boolean | null;
40
50
  }
51
+ export interface OrderByItem {
52
+ column: string;
53
+ direction: 'asc' | 'desc';
54
+ }
41
55
  export interface QueryResponse<T = any> {
42
56
  data: T[];
43
57
  count: number;
@@ -117,7 +131,7 @@ export declare class Table<T = any> {
117
131
  /**
118
132
  * Query with filter
119
133
  */
120
- filter(filter: FilterExpression): QueryBuilder<T>;
134
+ filter(column: string, operator: FilterExpression['operator'], value: any, logical_op?: 'AND' | 'OR'): QueryBuilder<T>;
121
135
  /**
122
136
  * Get all records (with optional limit)
123
137
  */
@@ -145,15 +159,39 @@ export declare class QueryBuilder<T = any> {
145
159
  private options;
146
160
  constructor(client: AxiosInstance, tableName: string);
147
161
  /**
148
- * Select specific columns
162
+ * Select specific columns or expressions
163
+ * @example query.select('id', 'name')
164
+ * @example query.select('category', 'COUNT(*) as count', 'AVG(price) as avg_price')
149
165
  */
150
- select(columns: string | string[]): this;
166
+ select(...columns: (string | string[])[]): this;
151
167
  /**
152
168
  * Add filter condition
169
+ * @example query.filter('age', 'gt', 18)
170
+ * @example query.filter('category', 'in', ['electronics', 'books'])
171
+ * @example query.filter('price', 'between', [10, 100])
172
+ */
173
+ filter(column: string, operator: FilterExpression['operator'], value: any, logical_op?: 'AND' | 'OR'): this;
174
+ /**
175
+ * Group results by column(s)
176
+ * @example query.groupBy('category')
177
+ * @example query.groupBy(['category', 'status'])
178
+ * @example query.groupBy('DATE(created_at)')
179
+ */
180
+ groupBy(columns: string | string[]): this;
181
+ /**
182
+ * Add HAVING clause filter (for filtering aggregated results)
183
+ * @example query.having('COUNT(*)', 'gt', 10)
184
+ * @example query.having('AVG(price)', 'gte', 50)
185
+ */
186
+ having(column: string, operator: HavingFilter['operator'], value: any): this;
187
+ /**
188
+ * Order by column(s)
189
+ * @example query.orderBy('created_at', 'desc')
190
+ * @example query.orderBy([{column: 'category', direction: 'asc'}, {column: 'price', direction: 'desc'}])
153
191
  */
154
- filter(filter: FilterExpression): this;
192
+ orderBy(column: string | OrderByItem[], direction?: 'asc' | 'desc'): this;
155
193
  /**
156
- * Order by column
194
+ * Order by a single column (alias for orderBy for backward compatibility)
157
195
  */
158
196
  order(column: string, direction?: 'asc' | 'desc'): this;
159
197
  /**
@@ -165,7 +203,7 @@ export declare class QueryBuilder<T = any> {
165
203
  */
166
204
  offset(offset: number): this;
167
205
  /**
168
- * Execute query
206
+ * Execute query - uses POST /{table}/query for advanced features, GET for simple queries
169
207
  */
170
208
  get(additionalOptions?: QueryOptions): Promise<QueryResponse<T>>;
171
209
  /**
package/dist/index.js CHANGED
@@ -125,8 +125,8 @@ class Table {
125
125
  /**
126
126
  * Query with filter
127
127
  */
128
- filter(filter) {
129
- return new QueryBuilder(this.client, this.tableName).filter(filter);
128
+ filter(column, operator, value, logical_op = 'AND') {
129
+ return new QueryBuilder(this.client, this.tableName).filter(column, operator, value, logical_op);
130
130
  }
131
131
  /**
132
132
  * Get all records (with optional limit)
@@ -172,19 +172,33 @@ class QueryBuilder {
172
172
  this.options = {};
173
173
  }
174
174
  /**
175
- * Select specific columns
175
+ * Select specific columns or expressions
176
+ * @example query.select('id', 'name')
177
+ * @example query.select('category', 'COUNT(*) as count', 'AVG(price) as avg_price')
176
178
  */
177
- select(columns) {
178
- this.options.select = Array.isArray(columns) ? columns.join(',') : columns;
179
+ select(...columns) {
180
+ if (columns.length === 1 && Array.isArray(columns[0])) {
181
+ this.options.select = columns[0];
182
+ }
183
+ else if (columns.length === 1 && typeof columns[0] === 'string') {
184
+ this.options.select = columns[0];
185
+ }
186
+ else {
187
+ this.options.select = columns;
188
+ }
179
189
  return this;
180
190
  }
181
191
  /**
182
192
  * Add filter condition
193
+ * @example query.filter('age', 'gt', 18)
194
+ * @example query.filter('category', 'in', ['electronics', 'books'])
195
+ * @example query.filter('price', 'between', [10, 100])
183
196
  */
184
- filter(filter) {
197
+ filter(column, operator, value, logical_op = 'AND') {
185
198
  if (!this.options.filter) {
186
199
  this.options.filter = [];
187
200
  }
201
+ const filter = { column, operator, value, logical_op };
188
202
  if (Array.isArray(this.options.filter)) {
189
203
  this.options.filter.push(filter);
190
204
  }
@@ -194,13 +208,56 @@ class QueryBuilder {
194
208
  return this;
195
209
  }
196
210
  /**
197
- * Order by column
211
+ * Group results by column(s)
212
+ * @example query.groupBy('category')
213
+ * @example query.groupBy(['category', 'status'])
214
+ * @example query.groupBy('DATE(created_at)')
198
215
  */
199
- order(column, direction = 'asc') {
200
- this.options.order = column;
201
- this.options.orderDirection = direction;
216
+ groupBy(columns) {
217
+ this.options.group_by = columns;
218
+ return this;
219
+ }
220
+ /**
221
+ * Add HAVING clause filter (for filtering aggregated results)
222
+ * @example query.having('COUNT(*)', 'gt', 10)
223
+ * @example query.having('AVG(price)', 'gte', 50)
224
+ */
225
+ having(column, operator, value) {
226
+ if (!this.options.having) {
227
+ this.options.having = [];
228
+ }
229
+ const havingFilter = { column, operator, value };
230
+ if (Array.isArray(this.options.having)) {
231
+ this.options.having.push(havingFilter);
232
+ }
233
+ else {
234
+ this.options.having = [this.options.having, havingFilter];
235
+ }
236
+ return this;
237
+ }
238
+ /**
239
+ * Order by column(s)
240
+ * @example query.orderBy('created_at', 'desc')
241
+ * @example query.orderBy([{column: 'category', direction: 'asc'}, {column: 'price', direction: 'desc'}])
242
+ */
243
+ orderBy(column, direction) {
244
+ if (typeof column === 'string') {
245
+ this.options.order = column;
246
+ if (direction) {
247
+ this.options.orderDirection = direction;
248
+ }
249
+ }
250
+ else {
251
+ this.options.order = column;
252
+ }
202
253
  return this;
203
254
  }
255
+ /**
256
+ * Order by a single column (alias for orderBy for backward compatibility)
257
+ */
258
+ order(column, direction = 'asc') {
259
+ return this.orderBy(column, direction);
260
+ }
204
261
  /**
205
262
  * Limit number of results
206
263
  */
@@ -216,35 +273,98 @@ class QueryBuilder {
216
273
  return this;
217
274
  }
218
275
  /**
219
- * Execute query
276
+ * Execute query - uses POST /{table}/query for advanced features, GET for simple queries
220
277
  */
221
278
  async get(additionalOptions) {
222
279
  const finalOptions = { ...this.options, ...additionalOptions };
223
- // Build query parameters
224
- const params = {};
280
+ // Build query request body for POST endpoint
281
+ const body = {};
282
+ // Select
225
283
  if (finalOptions.select) {
226
- params.select = finalOptions.select;
284
+ body.select = Array.isArray(finalOptions.select)
285
+ ? finalOptions.select
286
+ : typeof finalOptions.select === 'string'
287
+ ? finalOptions.select.split(',').map(s => s.trim())
288
+ : [finalOptions.select];
227
289
  }
290
+ // Filters
228
291
  if (finalOptions.filter) {
229
- const filters = Array.isArray(finalOptions.filter)
292
+ body.filters = Array.isArray(finalOptions.filter)
230
293
  ? finalOptions.filter
231
294
  : [finalOptions.filter];
232
- params.filter = filters
233
- .map((f) => `${f.column}.${f.operator}.${f.value}`)
234
- .join(',');
235
295
  }
296
+ // Group by
297
+ if (finalOptions.group_by) {
298
+ body.group_by = Array.isArray(finalOptions.group_by)
299
+ ? finalOptions.group_by
300
+ : typeof finalOptions.group_by === 'string'
301
+ ? finalOptions.group_by.split(',').map(s => s.trim())
302
+ : [finalOptions.group_by];
303
+ }
304
+ // Having
305
+ if (finalOptions.having) {
306
+ body.having = Array.isArray(finalOptions.having)
307
+ ? finalOptions.having
308
+ : [finalOptions.having];
309
+ }
310
+ // Order by
236
311
  if (finalOptions.order) {
237
- params.order = finalOptions.order;
238
- params.order_direction = finalOptions.orderDirection || 'asc';
312
+ if (typeof finalOptions.order === 'string') {
313
+ body.order_by = finalOptions.order;
314
+ body.order_direction = finalOptions.orderDirection || 'asc';
315
+ }
316
+ else {
317
+ body.order_by = finalOptions.order;
318
+ }
239
319
  }
320
+ // Limit and offset
240
321
  if (finalOptions.limit !== undefined) {
241
- params.limit = finalOptions.limit;
322
+ body.limit = finalOptions.limit;
242
323
  }
243
324
  if (finalOptions.offset !== undefined) {
244
- params.offset = finalOptions.offset;
325
+ body.offset = finalOptions.offset;
326
+ }
327
+ // Check if we need POST endpoint (advanced features)
328
+ const hasAdvancedFeatures = body.group_by ||
329
+ body.having ||
330
+ Array.isArray(body.order_by) ||
331
+ (body.filters && body.filters.some((f) => ['in', 'not_in', 'between', 'not_between'].includes(f.operator)));
332
+ if (hasAdvancedFeatures) {
333
+ // Use POST endpoint for advanced queries
334
+ const response = await this.client.post(`/${this.tableName}/query`, body);
335
+ return response.data;
336
+ }
337
+ else {
338
+ // Use GET endpoint for simple queries (backward compatibility)
339
+ const params = {};
340
+ if (body.select) {
341
+ params.select = Array.isArray(body.select) ? body.select.join(',') : body.select;
342
+ }
343
+ if (body.filters && body.filters.length > 0) {
344
+ // Check if any filter uses array values (can't use GET)
345
+ const hasArrayValues = body.filters.some((f) => Array.isArray(f.value));
346
+ if (hasArrayValues) {
347
+ // Must use POST
348
+ const response = await this.client.post(`/${this.tableName}/query`, body);
349
+ return response.data;
350
+ }
351
+ params.filter = body.filters
352
+ .map((f) => `${f.column}.${f.operator}.${f.value}`)
353
+ .join(',');
354
+ }
355
+ if (body.order_by && typeof body.order_by === 'string') {
356
+ params.order = body.order_by;
357
+ params.order_direction = body.order_direction || 'asc';
358
+ }
359
+ if (body.limit !== undefined) {
360
+ params.limit = body.limit;
361
+ }
362
+ if (body.offset !== undefined) {
363
+ params.offset = body.offset;
364
+ }
365
+ const response = await this.client.get(`/${this.tableName}`, { params });
366
+ return response.data;
245
367
  }
246
- const response = await this.client.get(`/${this.tableName}`, { params });
247
- return response.data;
248
368
  }
249
369
  /**
250
370
  * Get first record
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wowsql/sdk",
3
- "version": "3.5.0",
3
+ "version": "3.6.0",
4
4
  "description": "Official TypeScript/JavaScript SDK for WowSQL - MySQL Backend-as-a-Service with S3 Storage, type-safe queries and fluent API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",