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 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
+ [![npm version](https://img.shields.io/npm/v/laravel-query-builder-js.svg)](https://www.npmjs.com/package/laravel-query-builder-js)
4
+ [![CI](https://github.com/RanaUsman3131/laravel-query-builder-js/actions/workflows/ci.yml/badge.svg)](https://github.com/RanaUsman3131/laravel-query-builder-js/actions/workflows/ci.yml)
5
+ [![license](https://img.shields.io/npm/l/laravel-query-builder-js.svg)](./LICENSE)
6
+ [![types](https://img.shields.io/npm/types/laravel-query-builder-js.svg)](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