laravel-query-builder-js 1.0.1
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/LICENSE +21 -0
- package/README.md +388 -0
- package/dist/index.cjs +676 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +143 -0
- package/dist/index.d.ts +143 -0
- package/dist/index.js +672 -0
- package/dist/index.js.map +1 -0
- package/package.json +74 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rana Usman / Engage Platform
|
|
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,388 @@
|
|
|
1
|
+
# laravel-query-builder-js
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/laravel-query-builder-js)
|
|
4
|
+
[](https://github.com/RanaUsman3131/laravel-query-builder-js/actions/workflows/ci.yml)
|
|
5
|
+
[](./LICENSE)
|
|
6
|
+
[](https://www.npmjs.com/package/laravel-query-builder-js)
|
|
7
|
+
|
|
8
|
+
A fluent, **framework-agnostic**, Laravel-style query builder for JavaScript & TypeScript.
|
|
9
|
+
|
|
10
|
+
Compose `select`, `filter`, `sort`, `include` (eager-loaded relations) and pagination on the
|
|
11
|
+
client, then serialise it into query params (or a query string) for your API. It has **no
|
|
12
|
+
framework dependencies**, so the exact same code works in **React**, **Vue**, **Angular**,
|
|
13
|
+
and **Node.js**.
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
const query = new QueryBuilder()
|
|
17
|
+
.where("state", "=", "Open")
|
|
18
|
+
.whereIn("type", ["Buyer", "Tenant"])
|
|
19
|
+
.sort({ createdAt: "desc" })
|
|
20
|
+
.paginate(1, 25);
|
|
21
|
+
|
|
22
|
+
fetch("/api/leads?" + query.toQueryParams());
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Table of contents
|
|
28
|
+
|
|
29
|
+
- [Features](#features)
|
|
30
|
+
- [Installation](#installation)
|
|
31
|
+
- [Quick start](#quick-start)
|
|
32
|
+
- [Framework usage](#framework-usage)
|
|
33
|
+
- [React](#react)
|
|
34
|
+
- [Vue 3](#vue-3)
|
|
35
|
+
- [Angular](#angular)
|
|
36
|
+
- [Plain fetch / Node](#plain-fetch--node)
|
|
37
|
+
- [API reference](#api-reference)
|
|
38
|
+
- [Filtering](#filtering)
|
|
39
|
+
- [Grouped & nested conditions](#grouped--nested-conditions)
|
|
40
|
+
- [Selecting fields](#selecting-fields)
|
|
41
|
+
- [Sorting](#sorting)
|
|
42
|
+
- [Relations (include / withRelation)](#relations-include--withrelation)
|
|
43
|
+
- [Pagination](#pagination)
|
|
44
|
+
- [Serialisation](#serialisation)
|
|
45
|
+
- [Utilities](#utilities)
|
|
46
|
+
- [Operators](#operators)
|
|
47
|
+
- [Serialised output shape](#serialised-output-shape)
|
|
48
|
+
- [TypeScript](#typescript)
|
|
49
|
+
- [Contributing & development](#contributing--development)
|
|
50
|
+
- [Releasing](#releasing)
|
|
51
|
+
- [License](#license)
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Features
|
|
56
|
+
|
|
57
|
+
- ๐ **Fluent, chainable API** โ reads like a sentence.
|
|
58
|
+
- ๐งฉ **Nested relations** โ eager-load related resources with their own filters/sorts.
|
|
59
|
+
- ๐ชบ **Grouped conditions** โ build `(a OR b) AND c` with callbacks.
|
|
60
|
+
- ๐ **Automatic snake_case** โ select fields and sort keys are snake_cased for the backend.
|
|
61
|
+
- ๐ฆ **ESM + CJS + types** โ first-class TypeScript, works everywhere.
|
|
62
|
+
- ๐ชถ **Tiny footprint** โ one runtime dependency (`lodash`).
|
|
63
|
+
- ๐งช **Well tested** โ full unit-test suite.
|
|
64
|
+
|
|
65
|
+
## Installation
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npm install laravel-query-builder-js
|
|
69
|
+
# or
|
|
70
|
+
yarn add laravel-query-builder-js
|
|
71
|
+
# or
|
|
72
|
+
pnpm add laravel-query-builder-js
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Quick start
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
import { QueryBuilder } from "laravel-query-builder-js";
|
|
79
|
+
|
|
80
|
+
const query = new QueryBuilder()
|
|
81
|
+
.select(["id", "firstName", "email"])
|
|
82
|
+
.where("state", "=", "Open")
|
|
83
|
+
.whereIn("type", ["Buyer", "Tenant"])
|
|
84
|
+
.sort({ createdAt: "desc" })
|
|
85
|
+
.paginate(1, 25);
|
|
86
|
+
|
|
87
|
+
// As a Record<string, string> (great for axios `params`)
|
|
88
|
+
query.toParams();
|
|
89
|
+
// { select: '["id","first_name","email"]',
|
|
90
|
+
// filter: '{"and":[{"field":"state","op":"=","value":"Open"},{"field":"type","op":"in","value":["Buyer","Tenant"]}]}',
|
|
91
|
+
// sort: '{"created_at":"desc"}', page: 1, limit: 25, ... }
|
|
92
|
+
|
|
93
|
+
// As a URL-encoded query string
|
|
94
|
+
query.toQueryParams();
|
|
95
|
+
// select=%5B...%5D&filter=%7B...%7D&sort=%7B...%7D&page=1&limit=25
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Framework usage
|
|
99
|
+
|
|
100
|
+
The builder is plain TypeScript with zero framework coupling โ import it the same way anywhere.
|
|
101
|
+
|
|
102
|
+
### React
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
import { useMemo } from "react";
|
|
106
|
+
import { QueryBuilder } from "laravel-query-builder-js";
|
|
107
|
+
|
|
108
|
+
function useLeads(status: string, page: number) {
|
|
109
|
+
const params = useMemo(
|
|
110
|
+
() =>
|
|
111
|
+
new QueryBuilder()
|
|
112
|
+
.where("state", "=", status)
|
|
113
|
+
.sort({ createdAt: "desc" })
|
|
114
|
+
.paginate(page, 20)
|
|
115
|
+
.toParams(),
|
|
116
|
+
[status, page]
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
return useQuery(["leads", params], () =>
|
|
120
|
+
fetch("/api/leads?" + new URLSearchParams(params)).then((r) => r.json())
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Vue 3
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
import { computed } from "vue";
|
|
129
|
+
import { QueryBuilder } from "laravel-query-builder-js";
|
|
130
|
+
|
|
131
|
+
const status = ref("Open");
|
|
132
|
+
|
|
133
|
+
const queryString = computed(() =>
|
|
134
|
+
new QueryBuilder().where("state", "=", status.value).paginate(1, 20).toQueryParams()
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
watchEffect(() => fetch(`/api/leads?${queryString.value}`));
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Angular
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
import { HttpClient } from "@angular/common/http";
|
|
144
|
+
import { QueryBuilder } from "laravel-query-builder-js";
|
|
145
|
+
|
|
146
|
+
@Injectable({ providedIn: "root" })
|
|
147
|
+
export class LeadService {
|
|
148
|
+
constructor(private http: HttpClient) {}
|
|
149
|
+
|
|
150
|
+
getLeads(status: string, page = 1) {
|
|
151
|
+
const query = new QueryBuilder().where("state", "=", status).paginate(page, 20);
|
|
152
|
+
return this.http.get("/api/leads", { params: query.toParams() });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Plain fetch / Node
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
import { QueryBuilder } from "laravel-query-builder-js";
|
|
161
|
+
|
|
162
|
+
const query = new QueryBuilder().whereIn("id", [1, 2, 3]);
|
|
163
|
+
const res = await fetch(`https://api.example.com/users?${query.toQueryParams()}`);
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## API reference
|
|
167
|
+
|
|
168
|
+
Create a builder with the constructor or a static factory:
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
new QueryBuilder();
|
|
172
|
+
QueryBuilder.create("lead"); // optional resource name
|
|
173
|
+
QueryBuilder.new("lead"); // alias of create
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Every mutating method returns `this`, so calls chain.
|
|
177
|
+
|
|
178
|
+
### Filtering
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
q.where("state", "=", "Open"); // and: field = value
|
|
182
|
+
q.where("age", ">", 18);
|
|
183
|
+
q.orWhere("state", "=", "Pending"); // or: field = value
|
|
184
|
+
q.whereIn("type", ["Buyer", "Tenant"]); // and: field IN [...]
|
|
185
|
+
q.whereNull("deleted_at"); // and: field IS NULL
|
|
186
|
+
q.whereNotNull("email"); // and: field IS NOT NULL
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Adding a `where` with the same `field` **and** operator replaces the previous one (so you can
|
|
190
|
+
safely re-apply a filter as inputs change):
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
q.where("state", "=", "Open").where("state", "=", "Closed");
|
|
194
|
+
// -> only { field: "state", op: "=", value: "Closed" }
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Remove clauses again:
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
q.clearWhere("currency"); // remove and-clause(s) on a field
|
|
201
|
+
q.clearWhere("state", "="); // remove only the "=" clause on a field
|
|
202
|
+
q.clearOrWhere("state"); // remove or-clause(s) on a field
|
|
203
|
+
q.resetFilter(); // drop all filters
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Grouped & nested conditions
|
|
207
|
+
|
|
208
|
+
Pass a callback to build a parenthesised group. The nested builder exposes the same
|
|
209
|
+
`where` / `orWhere` methods:
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
// WHERE (division_id = 5 OR division_id IS NULL)
|
|
213
|
+
q.where((sub) => sub.orWhere("division_id", "=", 5).orWhere("division_id", "null"));
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Produces:
|
|
217
|
+
|
|
218
|
+
```jsonc
|
|
219
|
+
{ "and": [ { "or": [
|
|
220
|
+
{ "field": "division_id", "op": "=", "value": 5 },
|
|
221
|
+
{ "field": "division_id", "op": "null" }
|
|
222
|
+
] } ] }
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Selecting fields
|
|
226
|
+
|
|
227
|
+
Field names are converted to `snake_case` automatically:
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
q.select("firstName,lastName"); // -> ["first_name", "last_name"]
|
|
231
|
+
q.select(["id", "createdAt"]); // -> ["id", "created_at"]
|
|
232
|
+
q.addSelect(["extra_field"]); // append (raw, not snake_cased)
|
|
233
|
+
q.resetSelect(); // clear
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Sorting
|
|
237
|
+
|
|
238
|
+
Sort keys are snake_cased when serialised:
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
q.sort({ createdAt: "desc" }); // set (replaces)
|
|
242
|
+
q.addSort({ name: "asc" }); // merge into existing
|
|
243
|
+
q.resetSort(); // clear
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Relations (include / withRelation)
|
|
247
|
+
|
|
248
|
+
Eager-load related resources with their own nested select / filter / sort / include using
|
|
249
|
+
`withRelation`. Use the static `QueryBuilder.withRelation(name)` or the instance
|
|
250
|
+
`q.withRelation(name)`.
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
const q = new QueryBuilder();
|
|
254
|
+
|
|
255
|
+
q.addInclude(
|
|
256
|
+
q.withRelation("owner").where("status", "=", "Active").select(["id", "name"])
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
// Deeply nested relations
|
|
260
|
+
q.addInclude([
|
|
261
|
+
q
|
|
262
|
+
.withRelation("portal_profiles")
|
|
263
|
+
.addInclude(q.withRelation("portal"))
|
|
264
|
+
.addInclude(q.withRelation("plugin_config")),
|
|
265
|
+
]);
|
|
266
|
+
|
|
267
|
+
// Aggregates on a relation
|
|
268
|
+
q.addInclude(q.withRelation("deals").aggregate("count"));
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Methods:
|
|
272
|
+
|
|
273
|
+
- `include(rel | rel[])` โ set the includes (replaces).
|
|
274
|
+
- `addInclude(rel | rel[])` โ append includes.
|
|
275
|
+
- `resetInclude()` โ clear.
|
|
276
|
+
- `withRelation(name)` โ start a nested relation builder (chainable, same API).
|
|
277
|
+
- `.aggregate(value)` โ set an aggregate on a relation.
|
|
278
|
+
|
|
279
|
+
### Pagination
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
q.paginate(2, 25); // page = 2, limit = 25 (limit defaults to 15)
|
|
283
|
+
q.fromPage(3); // set only the page
|
|
284
|
+
q.page = 4; // accessor
|
|
285
|
+
q.limit = 50; // accessor
|
|
286
|
+
q.nextPage(); // increment page
|
|
287
|
+
q.previousPage(); // decrement page
|
|
288
|
+
q.resetPage(); // back to page 1
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Serialisation
|
|
292
|
+
|
|
293
|
+
```ts
|
|
294
|
+
q.toParams(); // Record<string, string> โ for axios `params` / URLSearchParams
|
|
295
|
+
q.toQueryParams(); // URL-encoded query string "a=...&b=..."
|
|
296
|
+
q.getQuery(); // the raw internal query object
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Utilities
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
q.clone(); // independent deep copy
|
|
303
|
+
q.addQueryBuilder(other); // merge another builder's select/filter/sort/include
|
|
304
|
+
q.resetQuery(); // reset everything (and re-apply default pagination)
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Operators
|
|
308
|
+
|
|
309
|
+
The `op` argument accepts any of:
|
|
310
|
+
|
|
311
|
+
```
|
|
312
|
+
% has > < = != <> >= <= null not_null
|
|
313
|
+
LIKE like BETWEEN IN in has|= has|< has|>
|
|
314
|
+
any any|like ANY doesntHave _like_
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## Serialised output shape
|
|
318
|
+
|
|
319
|
+
`toParams()` returns a `Record<string, string>` where each part is JSON-encoded:
|
|
320
|
+
|
|
321
|
+
| Key | When present | Example value |
|
|
322
|
+
| ----------- | ------------------------------- | -------------------------------------------------- |
|
|
323
|
+
| `select` | any select fields set | `["id","first_name"]` |
|
|
324
|
+
| `filter` | any filter set | `{"and":[{"field":"state","op":"=","value":"Open"}]}` |
|
|
325
|
+
| `sort` | any sort set | `{"created_at":"desc"}` (keys snake_cased) |
|
|
326
|
+
| `include` | any relation added | `[{"name":"owner","filter":{...}}]` |
|
|
327
|
+
| `page` | page is truthy | `2` |
|
|
328
|
+
| `limit` | limit is truthy | `25` |
|
|
329
|
+
|
|
330
|
+
`toQueryParams()` returns the same information as a single URL-encoded string
|
|
331
|
+
(`select=...&filter=...&sort=...&include=...&page=...&limit=...`).
|
|
332
|
+
|
|
333
|
+
## TypeScript
|
|
334
|
+
|
|
335
|
+
Fully typed. Public types are exported for building your own helpers:
|
|
336
|
+
|
|
337
|
+
```ts
|
|
338
|
+
import type {
|
|
339
|
+
Op,
|
|
340
|
+
Query,
|
|
341
|
+
QueryFilter,
|
|
342
|
+
QueryFilterGroup,
|
|
343
|
+
QueryInclude,
|
|
344
|
+
QuerySort,
|
|
345
|
+
} from "laravel-query-builder-js";
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
## Contributing & development
|
|
349
|
+
|
|
350
|
+
```bash
|
|
351
|
+
git clone https://github.com/RanaUsman3131/laravel-query-builder-js.git
|
|
352
|
+
cd laravel-query-builder-js
|
|
353
|
+
npm install
|
|
354
|
+
|
|
355
|
+
npm test # run the unit tests once
|
|
356
|
+
npm run test:watch # watch mode
|
|
357
|
+
npm run typecheck # tsc --noEmit
|
|
358
|
+
npm run build # bundle to dist/ (ESM + CJS + d.ts)
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
Please add tests for any new behaviour.
|
|
362
|
+
|
|
363
|
+
## Releasing
|
|
364
|
+
|
|
365
|
+
Releases are automated by GitHub Actions. Pushing a version tag builds, tests, publishes to
|
|
366
|
+
npm (with [provenance](https://docs.npmjs.com/generating-provenance-statements)), and creates
|
|
367
|
+
a GitHub Release.
|
|
368
|
+
|
|
369
|
+
```bash
|
|
370
|
+
# bump the version (updates package.json and creates the commit + tag)
|
|
371
|
+
npm version patch # or minor / major
|
|
372
|
+
git push --follow-tags
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
Or tag manually โ the tag **must** match `package.json`'s version:
|
|
376
|
+
|
|
377
|
+
```bash
|
|
378
|
+
git tag v1.2.3
|
|
379
|
+
git push origin v1.2.3
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
**One-time setup:** add an npm **Automation** access token as the repository secret
|
|
383
|
+
`NPM_TOKEN` (Settings โ Secrets and variables โ Actions). Provenance requires the repository
|
|
384
|
+
to be **public**.
|
|
385
|
+
|
|
386
|
+
## License
|
|
387
|
+
|
|
388
|
+
[MIT](./LICENSE) ยฉ Rana Usman / Engage Platform
|