@wowsql/sdk 3.5.0 → 3.7.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 +132 -0
- package/dist/auth.d.ts +68 -0
- package/dist/auth.js +139 -0
- package/dist/index.d.ts +113 -11
- package/dist/index.js +241 -24
- package/package.json +1 -1
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/auth.d.ts
CHANGED
|
@@ -146,6 +146,74 @@ export declare class ProjectAuthClient {
|
|
|
146
146
|
success: boolean;
|
|
147
147
|
message: string;
|
|
148
148
|
}>;
|
|
149
|
+
/**
|
|
150
|
+
* Send OTP code to user's email.
|
|
151
|
+
*
|
|
152
|
+
* Supports login, signup, and password_reset purposes.
|
|
153
|
+
*
|
|
154
|
+
* @param email - User's email address
|
|
155
|
+
* @param purpose - Purpose of OTP - 'login', 'signup', or 'password_reset' (default: 'login')
|
|
156
|
+
* @returns Object with success status and message
|
|
157
|
+
*/
|
|
158
|
+
sendOtp(email: string, purpose?: 'login' | 'signup' | 'password_reset'): Promise<{
|
|
159
|
+
success: boolean;
|
|
160
|
+
message: string;
|
|
161
|
+
}>;
|
|
162
|
+
/**
|
|
163
|
+
* Verify OTP and complete authentication.
|
|
164
|
+
*
|
|
165
|
+
* For signup: Creates new user if doesn't exist
|
|
166
|
+
* For login: Authenticates existing user
|
|
167
|
+
* For password_reset: Updates password if newPassword provided
|
|
168
|
+
*
|
|
169
|
+
* @param email - User's email address
|
|
170
|
+
* @param otp - 6-digit OTP code
|
|
171
|
+
* @param purpose - Purpose of OTP - 'login', 'signup', or 'password_reset' (default: 'login')
|
|
172
|
+
* @param newPassword - Required for password_reset purpose, new password (minimum 8 characters)
|
|
173
|
+
* @returns AuthResponse with session tokens and user info (for login/signup) or success message (for password_reset)
|
|
174
|
+
*/
|
|
175
|
+
verifyOtp(email: string, otp: string, purpose?: 'login' | 'signup' | 'password_reset', newPassword?: string): Promise<AuthResponse | {
|
|
176
|
+
success: boolean;
|
|
177
|
+
message: string;
|
|
178
|
+
}>;
|
|
179
|
+
/**
|
|
180
|
+
* Send magic link to user's email.
|
|
181
|
+
*
|
|
182
|
+
* Supports login, signup, and email_verification purposes.
|
|
183
|
+
*
|
|
184
|
+
* @param email - User's email address
|
|
185
|
+
* @param purpose - Purpose of magic link - 'login', 'signup', or 'email_verification' (default: 'login')
|
|
186
|
+
* @returns Object with success status and message
|
|
187
|
+
*/
|
|
188
|
+
sendMagicLink(email: string, purpose?: 'login' | 'signup' | 'email_verification'): Promise<{
|
|
189
|
+
success: boolean;
|
|
190
|
+
message: string;
|
|
191
|
+
}>;
|
|
192
|
+
/**
|
|
193
|
+
* Verify email using token (from magic link or OTP verification).
|
|
194
|
+
*
|
|
195
|
+
* Marks email as verified and sends welcome email.
|
|
196
|
+
*
|
|
197
|
+
* @param token - Verification token from email
|
|
198
|
+
* @returns Object with success status, message, and user info
|
|
199
|
+
*/
|
|
200
|
+
verifyEmail(token: string): Promise<{
|
|
201
|
+
success: boolean;
|
|
202
|
+
message: string;
|
|
203
|
+
user?: AuthUser;
|
|
204
|
+
}>;
|
|
205
|
+
/**
|
|
206
|
+
* Resend verification email.
|
|
207
|
+
*
|
|
208
|
+
* Always returns success to prevent email enumeration.
|
|
209
|
+
*
|
|
210
|
+
* @param email - User's email address
|
|
211
|
+
* @returns Object with success status and message
|
|
212
|
+
*/
|
|
213
|
+
resendVerification(email: string): Promise<{
|
|
214
|
+
success: boolean;
|
|
215
|
+
message: string;
|
|
216
|
+
}>;
|
|
149
217
|
/**
|
|
150
218
|
* Get the current session tokens.
|
|
151
219
|
*/
|
package/dist/auth.js
CHANGED
|
@@ -216,6 +216,145 @@ class ProjectAuthClient {
|
|
|
216
216
|
throw this.toWowError(error);
|
|
217
217
|
}
|
|
218
218
|
}
|
|
219
|
+
/**
|
|
220
|
+
* Send OTP code to user's email.
|
|
221
|
+
*
|
|
222
|
+
* Supports login, signup, and password_reset purposes.
|
|
223
|
+
*
|
|
224
|
+
* @param email - User's email address
|
|
225
|
+
* @param purpose - Purpose of OTP - 'login', 'signup', or 'password_reset' (default: 'login')
|
|
226
|
+
* @returns Object with success status and message
|
|
227
|
+
*/
|
|
228
|
+
async sendOtp(email, purpose = 'login') {
|
|
229
|
+
if (!['login', 'signup', 'password_reset'].includes(purpose)) {
|
|
230
|
+
throw new errors_1.WOWSQLError("Purpose must be 'login', 'signup', or 'password_reset'");
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
const response = await this.client.post('/otp/send', {
|
|
234
|
+
email,
|
|
235
|
+
purpose
|
|
236
|
+
});
|
|
237
|
+
return {
|
|
238
|
+
success: response.data.success ?? true,
|
|
239
|
+
message: response.data.message ?? 'If that email exists, an OTP code has been sent'
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
throw this.toWowError(error);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Verify OTP and complete authentication.
|
|
248
|
+
*
|
|
249
|
+
* For signup: Creates new user if doesn't exist
|
|
250
|
+
* For login: Authenticates existing user
|
|
251
|
+
* For password_reset: Updates password if newPassword provided
|
|
252
|
+
*
|
|
253
|
+
* @param email - User's email address
|
|
254
|
+
* @param otp - 6-digit OTP code
|
|
255
|
+
* @param purpose - Purpose of OTP - 'login', 'signup', or 'password_reset' (default: 'login')
|
|
256
|
+
* @param newPassword - Required for password_reset purpose, new password (minimum 8 characters)
|
|
257
|
+
* @returns AuthResponse with session tokens and user info (for login/signup) or success message (for password_reset)
|
|
258
|
+
*/
|
|
259
|
+
async verifyOtp(email, otp, purpose = 'login', newPassword) {
|
|
260
|
+
if (!['login', 'signup', 'password_reset'].includes(purpose)) {
|
|
261
|
+
throw new errors_1.WOWSQLError("Purpose must be 'login', 'signup', or 'password_reset'");
|
|
262
|
+
}
|
|
263
|
+
if (purpose === 'password_reset' && !newPassword) {
|
|
264
|
+
throw new errors_1.WOWSQLError("newPassword is required for password_reset purpose");
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
const payload = {
|
|
268
|
+
email,
|
|
269
|
+
otp,
|
|
270
|
+
purpose
|
|
271
|
+
};
|
|
272
|
+
if (newPassword) {
|
|
273
|
+
payload.new_password = newPassword;
|
|
274
|
+
}
|
|
275
|
+
const response = await this.client.post('/otp/verify', payload);
|
|
276
|
+
if (purpose === 'password_reset') {
|
|
277
|
+
return {
|
|
278
|
+
success: response.data.success ?? true,
|
|
279
|
+
message: response.data.message ?? 'Password reset successfully! You can now login with your new password'
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
const session = this.persistSession(response.data);
|
|
283
|
+
const user = response.data.user ? mapUser(response.data.user) : undefined;
|
|
284
|
+
return { user, session };
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
throw this.toWowError(error);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Send magic link to user's email.
|
|
292
|
+
*
|
|
293
|
+
* Supports login, signup, and email_verification purposes.
|
|
294
|
+
*
|
|
295
|
+
* @param email - User's email address
|
|
296
|
+
* @param purpose - Purpose of magic link - 'login', 'signup', or 'email_verification' (default: 'login')
|
|
297
|
+
* @returns Object with success status and message
|
|
298
|
+
*/
|
|
299
|
+
async sendMagicLink(email, purpose = 'login') {
|
|
300
|
+
if (!['login', 'signup', 'email_verification'].includes(purpose)) {
|
|
301
|
+
throw new errors_1.WOWSQLError("Purpose must be 'login', 'signup', or 'email_verification'");
|
|
302
|
+
}
|
|
303
|
+
try {
|
|
304
|
+
const response = await this.client.post('/magic-link/send', {
|
|
305
|
+
email,
|
|
306
|
+
purpose
|
|
307
|
+
});
|
|
308
|
+
return {
|
|
309
|
+
success: response.data.success ?? true,
|
|
310
|
+
message: response.data.message ?? 'If that email exists, a magic link has been sent'
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
throw this.toWowError(error);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Verify email using token (from magic link or OTP verification).
|
|
319
|
+
*
|
|
320
|
+
* Marks email as verified and sends welcome email.
|
|
321
|
+
*
|
|
322
|
+
* @param token - Verification token from email
|
|
323
|
+
* @returns Object with success status, message, and user info
|
|
324
|
+
*/
|
|
325
|
+
async verifyEmail(token) {
|
|
326
|
+
try {
|
|
327
|
+
const response = await this.client.post('/verify-email', { token });
|
|
328
|
+
return {
|
|
329
|
+
success: response.data.success ?? true,
|
|
330
|
+
message: response.data.message ?? 'Email verified successfully!',
|
|
331
|
+
user: response.data.user ? mapUser(response.data.user) : undefined
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
throw this.toWowError(error);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Resend verification email.
|
|
340
|
+
*
|
|
341
|
+
* Always returns success to prevent email enumeration.
|
|
342
|
+
*
|
|
343
|
+
* @param email - User's email address
|
|
344
|
+
* @returns Object with success status and message
|
|
345
|
+
*/
|
|
346
|
+
async resendVerification(email) {
|
|
347
|
+
try {
|
|
348
|
+
const response = await this.client.post('/resend-verification', { email });
|
|
349
|
+
return {
|
|
350
|
+
success: response.data.success ?? true,
|
|
351
|
+
message: response.data.message ?? 'If that email exists, a verification email has been sent'
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
throw this.toWowError(error);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
219
358
|
/**
|
|
220
359
|
* Get the current session tokens.
|
|
221
360
|
*/
|
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
|
-
/**
|
|
28
|
-
|
|
29
|
-
/**
|
|
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(
|
|
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
|
*/
|
|
@@ -130,6 +144,10 @@ export declare class Table<T = any> {
|
|
|
130
144
|
* Create a new record
|
|
131
145
|
*/
|
|
132
146
|
create(data: Partial<T>): Promise<CreateResponse>;
|
|
147
|
+
/**
|
|
148
|
+
* Insert a new record (alias for create)
|
|
149
|
+
*/
|
|
150
|
+
insert(data: Partial<T>): Promise<CreateResponse>;
|
|
133
151
|
/**
|
|
134
152
|
* Update a record by ID
|
|
135
153
|
*/
|
|
@@ -145,15 +163,39 @@ export declare class QueryBuilder<T = any> {
|
|
|
145
163
|
private options;
|
|
146
164
|
constructor(client: AxiosInstance, tableName: string);
|
|
147
165
|
/**
|
|
148
|
-
* Select specific columns
|
|
166
|
+
* Select specific columns or expressions
|
|
167
|
+
* @example query.select('id', 'name')
|
|
168
|
+
* @example query.select('category', 'COUNT(*) as count', 'AVG(price) as avg_price')
|
|
149
169
|
*/
|
|
150
|
-
select(columns: string | string[]): this;
|
|
170
|
+
select(...columns: (string | string[])[]): this;
|
|
151
171
|
/**
|
|
152
172
|
* Add filter condition
|
|
173
|
+
* @example query.filter('age', 'gt', 18)
|
|
174
|
+
* @example query.filter('category', 'in', ['electronics', 'books'])
|
|
175
|
+
* @example query.filter('price', 'between', [10, 100])
|
|
153
176
|
*/
|
|
154
|
-
filter(
|
|
177
|
+
filter(column: string, operator: FilterExpression['operator'], value: any, logical_op?: 'AND' | 'OR'): this;
|
|
155
178
|
/**
|
|
156
|
-
*
|
|
179
|
+
* Group results by column(s)
|
|
180
|
+
* @example query.groupBy('category')
|
|
181
|
+
* @example query.groupBy(['category', 'status'])
|
|
182
|
+
* @example query.groupBy('DATE(created_at)')
|
|
183
|
+
*/
|
|
184
|
+
groupBy(columns: string | string[]): this;
|
|
185
|
+
/**
|
|
186
|
+
* Add HAVING clause filter (for filtering aggregated results)
|
|
187
|
+
* @example query.having('COUNT(*)', 'gt', 10)
|
|
188
|
+
* @example query.having('AVG(price)', 'gte', 50)
|
|
189
|
+
*/
|
|
190
|
+
having(column: string, operator: HavingFilter['operator'], value: any): this;
|
|
191
|
+
/**
|
|
192
|
+
* Order by column(s)
|
|
193
|
+
* @example query.orderBy('created_at', 'desc')
|
|
194
|
+
* @example query.orderBy([{column: 'category', direction: 'asc'}, {column: 'price', direction: 'desc'}])
|
|
195
|
+
*/
|
|
196
|
+
orderBy(column: string | OrderByItem[], direction?: 'asc' | 'desc'): this;
|
|
197
|
+
/**
|
|
198
|
+
* Order by a single column (alias for orderBy for backward compatibility)
|
|
157
199
|
*/
|
|
158
200
|
order(column: string, direction?: 'asc' | 'desc'): this;
|
|
159
201
|
/**
|
|
@@ -165,9 +207,69 @@ export declare class QueryBuilder<T = any> {
|
|
|
165
207
|
*/
|
|
166
208
|
offset(offset: number): this;
|
|
167
209
|
/**
|
|
168
|
-
*
|
|
210
|
+
* Filter where column equals value
|
|
211
|
+
*/
|
|
212
|
+
eq(column: string, value: any): this;
|
|
213
|
+
/**
|
|
214
|
+
* Filter where column does not equal value
|
|
215
|
+
*/
|
|
216
|
+
neq(column: string, value: any): this;
|
|
217
|
+
/**
|
|
218
|
+
* Filter where column is greater than value
|
|
219
|
+
*/
|
|
220
|
+
gt(column: string, value: any): this;
|
|
221
|
+
/**
|
|
222
|
+
* Filter where column is greater than or equal to value
|
|
223
|
+
*/
|
|
224
|
+
gte(column: string, value: any): this;
|
|
225
|
+
/**
|
|
226
|
+
* Filter where column is less than value
|
|
227
|
+
*/
|
|
228
|
+
lt(column: string, value: any): this;
|
|
229
|
+
/**
|
|
230
|
+
* Filter where column is less than or equal to value
|
|
231
|
+
*/
|
|
232
|
+
lte(column: string, value: any): this;
|
|
233
|
+
/**
|
|
234
|
+
* Filter where column matches pattern (SQL LIKE)
|
|
235
|
+
*/
|
|
236
|
+
like(column: string, value: string): this;
|
|
237
|
+
/**
|
|
238
|
+
* Filter where column IS NULL
|
|
239
|
+
*/
|
|
240
|
+
isNull(column: string): this;
|
|
241
|
+
/**
|
|
242
|
+
* Filter where column IS NOT NULL
|
|
243
|
+
*/
|
|
244
|
+
isNotNull(column: string): this;
|
|
245
|
+
/**
|
|
246
|
+
* Filter where column is in list of values
|
|
247
|
+
*/
|
|
248
|
+
in(column: string, values: any[]): this;
|
|
249
|
+
/**
|
|
250
|
+
* Filter where column is not in list of values
|
|
251
|
+
*/
|
|
252
|
+
notIn(column: string, values: any[]): this;
|
|
253
|
+
/**
|
|
254
|
+
* Filter where column is between min and max values
|
|
255
|
+
*/
|
|
256
|
+
between(column: string, minValue: any, maxValue: any): this;
|
|
257
|
+
/**
|
|
258
|
+
* Filter where column is not between min and max values
|
|
259
|
+
*/
|
|
260
|
+
notBetween(column: string, minValue: any, maxValue: any): this;
|
|
261
|
+
/**
|
|
262
|
+
* Add an OR filter condition
|
|
263
|
+
*/
|
|
264
|
+
or(column: string, operator: FilterExpression['operator'], value: any): this;
|
|
265
|
+
/**
|
|
266
|
+
* Execute query - uses POST /{table}/query for advanced features, GET for simple queries
|
|
169
267
|
*/
|
|
170
268
|
get(additionalOptions?: QueryOptions): Promise<QueryResponse<T>>;
|
|
269
|
+
/**
|
|
270
|
+
* Execute the query (alias for get)
|
|
271
|
+
*/
|
|
272
|
+
execute(additionalOptions?: QueryOptions): Promise<QueryResponse<T>>;
|
|
171
273
|
/**
|
|
172
274
|
* Get first record
|
|
173
275
|
*/
|
package/dist/index.js
CHANGED
|
@@ -125,8 +125,8 @@ class Table {
|
|
|
125
125
|
/**
|
|
126
126
|
* Query with filter
|
|
127
127
|
*/
|
|
128
|
-
filter(
|
|
129
|
-
return new QueryBuilder(this.client, this.tableName).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)
|
|
@@ -148,6 +148,12 @@ class Table {
|
|
|
148
148
|
const response = await this.client.post(`/${this.tableName}`, data);
|
|
149
149
|
return response.data;
|
|
150
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Insert a new record (alias for create)
|
|
153
|
+
*/
|
|
154
|
+
async insert(data) {
|
|
155
|
+
return this.create(data);
|
|
156
|
+
}
|
|
151
157
|
/**
|
|
152
158
|
* Update a record by ID
|
|
153
159
|
*/
|
|
@@ -172,19 +178,33 @@ class QueryBuilder {
|
|
|
172
178
|
this.options = {};
|
|
173
179
|
}
|
|
174
180
|
/**
|
|
175
|
-
* Select specific columns
|
|
181
|
+
* Select specific columns or expressions
|
|
182
|
+
* @example query.select('id', 'name')
|
|
183
|
+
* @example query.select('category', 'COUNT(*) as count', 'AVG(price) as avg_price')
|
|
176
184
|
*/
|
|
177
|
-
select(columns) {
|
|
178
|
-
|
|
185
|
+
select(...columns) {
|
|
186
|
+
if (columns.length === 1 && Array.isArray(columns[0])) {
|
|
187
|
+
this.options.select = columns[0];
|
|
188
|
+
}
|
|
189
|
+
else if (columns.length === 1 && typeof columns[0] === 'string') {
|
|
190
|
+
this.options.select = columns[0];
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
this.options.select = columns;
|
|
194
|
+
}
|
|
179
195
|
return this;
|
|
180
196
|
}
|
|
181
197
|
/**
|
|
182
198
|
* Add filter condition
|
|
199
|
+
* @example query.filter('age', 'gt', 18)
|
|
200
|
+
* @example query.filter('category', 'in', ['electronics', 'books'])
|
|
201
|
+
* @example query.filter('price', 'between', [10, 100])
|
|
183
202
|
*/
|
|
184
|
-
filter(
|
|
203
|
+
filter(column, operator, value, logical_op = 'AND') {
|
|
185
204
|
if (!this.options.filter) {
|
|
186
205
|
this.options.filter = [];
|
|
187
206
|
}
|
|
207
|
+
const filter = { column, operator, value, logical_op };
|
|
188
208
|
if (Array.isArray(this.options.filter)) {
|
|
189
209
|
this.options.filter.push(filter);
|
|
190
210
|
}
|
|
@@ -194,13 +214,56 @@ class QueryBuilder {
|
|
|
194
214
|
return this;
|
|
195
215
|
}
|
|
196
216
|
/**
|
|
197
|
-
*
|
|
217
|
+
* Group results by column(s)
|
|
218
|
+
* @example query.groupBy('category')
|
|
219
|
+
* @example query.groupBy(['category', 'status'])
|
|
220
|
+
* @example query.groupBy('DATE(created_at)')
|
|
198
221
|
*/
|
|
199
|
-
|
|
200
|
-
this.options.
|
|
201
|
-
this
|
|
222
|
+
groupBy(columns) {
|
|
223
|
+
this.options.group_by = columns;
|
|
224
|
+
return this;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Add HAVING clause filter (for filtering aggregated results)
|
|
228
|
+
* @example query.having('COUNT(*)', 'gt', 10)
|
|
229
|
+
* @example query.having('AVG(price)', 'gte', 50)
|
|
230
|
+
*/
|
|
231
|
+
having(column, operator, value) {
|
|
232
|
+
if (!this.options.having) {
|
|
233
|
+
this.options.having = [];
|
|
234
|
+
}
|
|
235
|
+
const havingFilter = { column, operator, value };
|
|
236
|
+
if (Array.isArray(this.options.having)) {
|
|
237
|
+
this.options.having.push(havingFilter);
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
this.options.having = [this.options.having, havingFilter];
|
|
241
|
+
}
|
|
242
|
+
return this;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Order by column(s)
|
|
246
|
+
* @example query.orderBy('created_at', 'desc')
|
|
247
|
+
* @example query.orderBy([{column: 'category', direction: 'asc'}, {column: 'price', direction: 'desc'}])
|
|
248
|
+
*/
|
|
249
|
+
orderBy(column, direction) {
|
|
250
|
+
if (typeof column === 'string') {
|
|
251
|
+
this.options.order = column;
|
|
252
|
+
if (direction) {
|
|
253
|
+
this.options.orderDirection = direction;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
this.options.order = column;
|
|
258
|
+
}
|
|
202
259
|
return this;
|
|
203
260
|
}
|
|
261
|
+
/**
|
|
262
|
+
* Order by a single column (alias for orderBy for backward compatibility)
|
|
263
|
+
*/
|
|
264
|
+
order(column, direction = 'asc') {
|
|
265
|
+
return this.orderBy(column, direction);
|
|
266
|
+
}
|
|
204
267
|
/**
|
|
205
268
|
* Limit number of results
|
|
206
269
|
*/
|
|
@@ -215,36 +278,190 @@ class QueryBuilder {
|
|
|
215
278
|
this.options.offset = offset;
|
|
216
279
|
return this;
|
|
217
280
|
}
|
|
281
|
+
// ==================== Convenience Methods ====================
|
|
282
|
+
/**
|
|
283
|
+
* Filter where column equals value
|
|
284
|
+
*/
|
|
285
|
+
eq(column, value) {
|
|
286
|
+
return this.filter(column, 'eq', value);
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Filter where column does not equal value
|
|
290
|
+
*/
|
|
291
|
+
neq(column, value) {
|
|
292
|
+
return this.filter(column, 'neq', value);
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Filter where column is greater than value
|
|
296
|
+
*/
|
|
297
|
+
gt(column, value) {
|
|
298
|
+
return this.filter(column, 'gt', value);
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Filter where column is greater than or equal to value
|
|
302
|
+
*/
|
|
303
|
+
gte(column, value) {
|
|
304
|
+
return this.filter(column, 'gte', value);
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Filter where column is less than value
|
|
308
|
+
*/
|
|
309
|
+
lt(column, value) {
|
|
310
|
+
return this.filter(column, 'lt', value);
|
|
311
|
+
}
|
|
218
312
|
/**
|
|
219
|
-
*
|
|
313
|
+
* Filter where column is less than or equal to value
|
|
314
|
+
*/
|
|
315
|
+
lte(column, value) {
|
|
316
|
+
return this.filter(column, 'lte', value);
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Filter where column matches pattern (SQL LIKE)
|
|
320
|
+
*/
|
|
321
|
+
like(column, value) {
|
|
322
|
+
return this.filter(column, 'like', value);
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Filter where column IS NULL
|
|
326
|
+
*/
|
|
327
|
+
isNull(column) {
|
|
328
|
+
return this.filter(column, 'is', null);
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Filter where column IS NOT NULL
|
|
332
|
+
*/
|
|
333
|
+
isNotNull(column) {
|
|
334
|
+
return this.filter(column, 'is_not', null);
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Filter where column is in list of values
|
|
338
|
+
*/
|
|
339
|
+
in(column, values) {
|
|
340
|
+
return this.filter(column, 'in', values);
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Filter where column is not in list of values
|
|
344
|
+
*/
|
|
345
|
+
notIn(column, values) {
|
|
346
|
+
return this.filter(column, 'not_in', values);
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Filter where column is between min and max values
|
|
350
|
+
*/
|
|
351
|
+
between(column, minValue, maxValue) {
|
|
352
|
+
return this.filter(column, 'between', [minValue, maxValue]);
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Filter where column is not between min and max values
|
|
356
|
+
*/
|
|
357
|
+
notBetween(column, minValue, maxValue) {
|
|
358
|
+
return this.filter(column, 'not_between', [minValue, maxValue]);
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Add an OR filter condition
|
|
362
|
+
*/
|
|
363
|
+
or(column, operator, value) {
|
|
364
|
+
return this.filter(column, operator, value, 'OR');
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Execute query - uses POST /{table}/query for advanced features, GET for simple queries
|
|
220
368
|
*/
|
|
221
369
|
async get(additionalOptions) {
|
|
222
370
|
const finalOptions = { ...this.options, ...additionalOptions };
|
|
223
|
-
// Build query
|
|
224
|
-
const
|
|
371
|
+
// Build query request body for POST endpoint
|
|
372
|
+
const body = {};
|
|
373
|
+
// Select
|
|
225
374
|
if (finalOptions.select) {
|
|
226
|
-
|
|
375
|
+
body.select = Array.isArray(finalOptions.select)
|
|
376
|
+
? finalOptions.select
|
|
377
|
+
: typeof finalOptions.select === 'string'
|
|
378
|
+
? finalOptions.select.split(',').map(s => s.trim())
|
|
379
|
+
: [finalOptions.select];
|
|
227
380
|
}
|
|
381
|
+
// Filters
|
|
228
382
|
if (finalOptions.filter) {
|
|
229
|
-
|
|
383
|
+
body.filters = Array.isArray(finalOptions.filter)
|
|
230
384
|
? finalOptions.filter
|
|
231
385
|
: [finalOptions.filter];
|
|
232
|
-
params.filter = filters
|
|
233
|
-
.map((f) => `${f.column}.${f.operator}.${f.value}`)
|
|
234
|
-
.join(',');
|
|
235
386
|
}
|
|
387
|
+
// Group by
|
|
388
|
+
if (finalOptions.group_by) {
|
|
389
|
+
body.group_by = Array.isArray(finalOptions.group_by)
|
|
390
|
+
? finalOptions.group_by
|
|
391
|
+
: typeof finalOptions.group_by === 'string'
|
|
392
|
+
? finalOptions.group_by.split(',').map(s => s.trim())
|
|
393
|
+
: [finalOptions.group_by];
|
|
394
|
+
}
|
|
395
|
+
// Having
|
|
396
|
+
if (finalOptions.having) {
|
|
397
|
+
body.having = Array.isArray(finalOptions.having)
|
|
398
|
+
? finalOptions.having
|
|
399
|
+
: [finalOptions.having];
|
|
400
|
+
}
|
|
401
|
+
// Order by
|
|
236
402
|
if (finalOptions.order) {
|
|
237
|
-
|
|
238
|
-
|
|
403
|
+
if (typeof finalOptions.order === 'string') {
|
|
404
|
+
body.order_by = finalOptions.order;
|
|
405
|
+
body.order_direction = finalOptions.orderDirection || 'asc';
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
body.order_by = finalOptions.order;
|
|
409
|
+
}
|
|
239
410
|
}
|
|
411
|
+
// Limit and offset
|
|
240
412
|
if (finalOptions.limit !== undefined) {
|
|
241
|
-
|
|
413
|
+
body.limit = finalOptions.limit;
|
|
242
414
|
}
|
|
243
415
|
if (finalOptions.offset !== undefined) {
|
|
244
|
-
|
|
416
|
+
body.offset = finalOptions.offset;
|
|
245
417
|
}
|
|
246
|
-
|
|
247
|
-
|
|
418
|
+
// Check if we need POST endpoint (advanced features)
|
|
419
|
+
const hasAdvancedFeatures = body.group_by ||
|
|
420
|
+
body.having ||
|
|
421
|
+
Array.isArray(body.order_by) ||
|
|
422
|
+
(body.filters && body.filters.some((f) => ['in', 'not_in', 'between', 'not_between'].includes(f.operator)));
|
|
423
|
+
if (hasAdvancedFeatures) {
|
|
424
|
+
// Use POST endpoint for advanced queries
|
|
425
|
+
const response = await this.client.post(`/${this.tableName}/query`, body);
|
|
426
|
+
return response.data;
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
// Use GET endpoint for simple queries (backward compatibility)
|
|
430
|
+
const params = {};
|
|
431
|
+
if (body.select) {
|
|
432
|
+
params.select = Array.isArray(body.select) ? body.select.join(',') : body.select;
|
|
433
|
+
}
|
|
434
|
+
if (body.filters && body.filters.length > 0) {
|
|
435
|
+
// Check if any filter uses array values (can't use GET)
|
|
436
|
+
const hasArrayValues = body.filters.some((f) => Array.isArray(f.value));
|
|
437
|
+
if (hasArrayValues) {
|
|
438
|
+
// Must use POST
|
|
439
|
+
const response = await this.client.post(`/${this.tableName}/query`, body);
|
|
440
|
+
return response.data;
|
|
441
|
+
}
|
|
442
|
+
params.filter = body.filters
|
|
443
|
+
.map((f) => `${f.column}.${f.operator}.${f.value}`)
|
|
444
|
+
.join(',');
|
|
445
|
+
}
|
|
446
|
+
if (body.order_by && typeof body.order_by === 'string') {
|
|
447
|
+
params.order = body.order_by;
|
|
448
|
+
params.order_direction = body.order_direction || 'asc';
|
|
449
|
+
}
|
|
450
|
+
if (body.limit !== undefined) {
|
|
451
|
+
params.limit = body.limit;
|
|
452
|
+
}
|
|
453
|
+
if (body.offset !== undefined) {
|
|
454
|
+
params.offset = body.offset;
|
|
455
|
+
}
|
|
456
|
+
const response = await this.client.get(`/${this.tableName}`, { params });
|
|
457
|
+
return response.data;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Execute the query (alias for get)
|
|
462
|
+
*/
|
|
463
|
+
async execute(additionalOptions) {
|
|
464
|
+
return this.get(additionalOptions);
|
|
248
465
|
}
|
|
249
466
|
/**
|
|
250
467
|
* Get first record
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wowsql/sdk",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.7.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",
|