{"_id":"@bir-tan/crisp-oquent","name":"@bir-tan/crisp-oquent","dist-tags":{"latest":"2.1.0"},"versions":{"2.1.0":{"name":"@bir-tan/crisp-oquent","version":"2.1.0","publishConfig":{"access":"public"},"description":"Fetch-only TypeScript API client that speaks Spatie laravel-query-builder's URL contract — fluent Eloquent-style builder with Filter Groups (JSON:API Fancy Filters).","type":"module","main":"./dist/index.js","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"engines":{"node":">=18"},"sideEffects":false,"scripts":{"build":"tsc","build:watch":"tsc --watch","clean":"rm -rf dist","test":"vitest run","test:watch":"vitest","typecheck":"tsc --noEmit","prepublishOnly":"npm run clean && npm run build"},"repository":{"type":"git","url":"git+https://github.com/taskinbirtan/crisp-oquent.git"},"keywords":["typescript","fetch","laravel","spatie","query-builder","eloquent","json-api","api-client","nuxt","next","vue","react"],"author":{"name":"Birtan Taskin"},"license":"Apache-2.0","bugs":{"url":"https://github.com/taskinbirtan/crisp-oquent/issues"},"homepage":"https://github.com/taskinbirtan/crisp-oquent#readme","devDependencies":{"typescript":"^5.6.0","vitest":"^2.1.0"},"gitHead":"1dd768d2eab65215a39fa1ec86ddce40631b1fb8","_id":"@bir-tan/crisp-oquent@2.1.0","_nodeVersion":"25.8.1","_npmVersion":"11.11.0","dist":{"integrity":"sha512-ZrvAaFHDTJ1tzRl4npriNyyv3T4bVPSQ1coMpDOPWHFobq3g9zxYm3kR3uNjfm1VMZInqonXqC/4d20REVESJw==","shasum":"534310c502603f50340fa2ba8af2ed1d4a0d96ec","tarball":"https://registry.npmjs.org/@bir-tan/crisp-oquent/-/crisp-oquent-2.1.0.tgz","fileCount":39,"unpackedSize":76601,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIEaUPa1iFq/X+ur2EAvSPliLZhDaztQ0MXhX4Ir7fyVhAiAs+3g9FILZ56YVACiG7ltXqW+z4+M4nZEsPRFJ8tcdFQ=="}]},"_npmUser":{"name":"bir-tan","email":"taskinbirtan@gmail.com"},"directories":{},"maintainers":[{"name":"bir-tan","email":"taskinbirtan@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/crisp-oquent_2.1.0_1777746762156_0.6044675323101039"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-02T18:32:41.949Z","2.1.0":"2026-05-02T18:32:42.301Z","modified":"2026-05-02T18:32:42.560Z"},"maintainers":[{"name":"bir-tan","email":"taskinbirtan@gmail.com"}],"description":"Fetch-only TypeScript API client that speaks Spatie laravel-query-builder's URL contract — fluent Eloquent-style builder with Filter Groups (JSON:API Fancy Filters).","homepage":"https://github.com/taskinbirtan/crisp-oquent#readme","keywords":["typescript","fetch","laravel","spatie","query-builder","eloquent","json-api","api-client","nuxt","next","vue","react"],"repository":{"type":"git","url":"git+https://github.com/taskinbirtan/crisp-oquent.git"},"author":{"name":"Birtan Taskin"},"bugs":{"url":"https://github.com/taskinbirtan/crisp-oquent/issues"},"license":"Apache-2.0","readme":"# crisp-oquent\n\n> A **fetch-only**, TypeScript-first API client that speaks Spatie [`laravel-query-builder`](https://github.com/spatie/laravel-query-builder)'s URL contract — including JSON:API Fancy Filter Groups (Spatie v7.3.0 / PR [#1060](https://github.com/spatie/laravel-query-builder/pull/1060)).\n\n[![npm version](https://img.shields.io/npm/v/%40bir-tan%2Fcrisp-oquent.svg)](https://www.npmjs.com/package/@bir-tan/crisp-oquent)\n[![license](https://img.shields.io/npm/l/%40bir-tan%2Fcrisp-oquent.svg)](./LICENSE)\n\n- **Zero dependencies.** Just `fetch` — no Axios, no polyfills.\n- **ESM-only**, strict TypeScript, ships its own `.d.ts`. Node ≥ 18 and modern browsers.\n- **Full Spatie v7 feature parity:** `filter[…]`, dynamic operators, trashed, nullable, sort, include (incl. count/exists/sum/avg/min/max), sparse fieldsets, append.\n- **Filter groups:** `filterGroup()` shorthand for server-side `AllowedFilter::groupOr / groupAnd` (Spatie v7.3.0 / PR [#1060](https://github.com/spatie/laravel-query-builder/pull/1060)).\n- **Laravel-aware pagination** — parses Laravel API Resource paginated responses out of the box.\n- **Auth + interceptors + structured errors:** bearer token, request/response middleware, `HttpError` with helpers like `isValidationError`.\n\n## Install\n\n```bash\nnpm install @bir-tan/crisp-oquent\n```\n\n## Quick start\n\n### 1. Configure once\n\n```ts\nimport { CrispOquentConfig } from '@bir-tan/crisp-oquent';\n\nCrispOquentConfig.initialize({ baseUri: 'https://api.example.com' });\nCrispOquentConfig.setBearerToken(localStorage.getItem('token'));\n```\n\n### 2. Define a model\n\n```ts\nimport { Model } from '@bir-tan/crisp-oquent';\n\nexport class User extends Model {\n  static override uri = '/users';\n\n  declare id?: number;\n  declare name?: string;\n  declare email?: string;\n}\n```\n\n### 3. Fluent queries — Spatie URL contract\n\n```ts\nconst users = await User.crispy()\n  .filter('status', 'active')        // filter[status]=active\n  .filter('id', [1, 2, 3])           // filter[id]=1,2,3\n  .sortByDesc('created_at')          // sort=-created_at\n  .sortBy('name')                    // sort=-created_at,name\n  .include('posts', 'profile')       // include=posts,profile\n  .fields('users', 'id', 'name')     // fields[users]=id,name\n  .append('full_name')               // append=full_name\n  .get();\n```\n\n### 4. Pagination\n\n```ts\nconst page = await User.crispy().filter('active', true).paginate(2, 25);\n\npage.items;          // User[]\npage.currentPage;    // 2\npage.perPage;        // 25\npage.total;          // 137\npage.lastPage;       // 6\npage.hasMorePages(); // true\npage.links.next;     // 'https://api.example.com/users?page=3'\n```\n\n### 5. Dynamic operator filters\n\nPairs with `AllowedFilter::operator($name, FilterOperator::DYNAMIC)` on the server.\n\n```ts\nimport { FilterOperator } from '@bir-tan/crisp-oquent';\n\nawait User.crispy()\n  .where('salary', FilterOperator.GREATER_THAN, 3000)        // filter[salary]=>3000\n  .where('id', FilterOperator.NOT_EQUAL, 7)                  // filter[id]=!=7\n  .where('created_at', FilterOperator.LESS_THAN_OR_EQUAL, '2026-01-01')\n  .get();\n```\n\nAvailable operators: `EQUAL`, `NOT_EQUAL`, `GREATER_THAN`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN`, `LESS_THAN_OR_EQUAL`.\n\n### 6. Trashed (SoftDeletes) and nullable filters\n\n```ts\nawait User.crispy().withTrashed().get();   // filter[trashed]=with\nawait User.crispy().onlyTrashed().get();   // filter[trashed]=only\n\nawait User.crispy().whereNull('deleted_at').get();   // filter[deleted_at]=null\nawait User.crispy().whereNotNull('email').get();     // filter[email]=not-null\n```\n\n### 7. Aggregate includes (sum / avg / min / max / count / exists)\n\nPairs with Spatie's `AllowedInclude::sum / avg / min / max / count / exists`. Pass the include name declared on the server.\n\n```ts\nawait User.crispy()\n  .includeCount('posts')                    // postsCount\n  .includeExists('friends')                 // friendsExists\n  .includeSum('postsViewsSum')              // postsViewsSum\n  .includeAvg('postsViewsAvg')\n  .includeMin('postsViewsMin')\n  .includeMax('postsViewsMax')\n  .get();\n// → ?include=postsCount,friendsExists,postsViewsSum,postsViewsAvg,postsViewsMin,postsViewsMax\n```\n\n### 8. Custom array delimiter (Spatie v7.2.0)\n\nMirror your server-side `query-builder.array_value_delimiter` config:\n\n```ts\nCrispOquentConfig.setFilterDelimiter('|');\nawait User.crispy().filter('id', [1, 2, 3]).get();   // filter[id]=1|2|3\n\n// Per-builder override:\nawait User.crispy().delimiter(';').filter('id', [1, 2]).get();\n```\n\n### 9. Filter Groups (Spatie v7.3.0 — PR #1060)\n\nOn the backend:\n\n```php\nQueryBuilder::for(User::class)\n    ->allowedFilters(\n        AllowedFilter::groupOr('q', [\n            AllowedFilter::partial('name'),\n            AllowedFilter::partial('full_name'),\n        ]),\n    );\n```\n\nOn the client, one line:\n\n```ts\nconst matches = await User.crispy().filterGroup('q', 'John').get();\n// → GET /users?filter[q]=John\n// → backend WHERE (name LIKE '%John%' OR full_name LIKE '%John%')\n```\n\nThe conjunction (AND/OR), which fields the shorthand fans out to, and value broadcasting all live server-side. The client just sends the shorthand; the composition is owned by `FiltersGroup`.\n\n### 10. Single records & CRUD\n\n```ts\nconst user = await User.crispy().find(42);  // GET /users/42 (404 → null)\nconst first = await User.crispy().filter('active', true).first();\n\n// Create\nconst fresh = await User.crispy().create({ name: 'Birtan', email: 'b@x' });\n\n// Update\nfresh.name = 'Birtan T.';\nawait fresh.save();   // PUT /users/:id\n\n// Delete\nawait fresh.delete(); // DELETE /users/:id\n```\n\n### 11. Auth & interceptors\n\n```ts\nCrispOquentConfig.setBearerToken('abc123');\n\nCrispOquentConfig.addRequestInterceptor((ctx) => ({\n  ...ctx,\n  init: {\n    ...ctx.init,\n    headers: { ...ctx.init.headers, 'X-Trace-Id': crypto.randomUUID() },\n  },\n}));\n\nCrispOquentConfig.addResponseInterceptor(async (response) => {\n  if (response.status === 401) {\n    // refresh, redirect, …\n  }\n  return response;\n});\n```\n\n### 12. Error handling\n\n```ts\nimport { HttpError } from '@bir-tan/crisp-oquent';\n\ntry {\n  await new User({ email: '' }).save();\n} catch (e) {\n  if (e instanceof HttpError && e.isValidationError) {\n    e.validationErrors; // { email: ['required'] }\n  }\n}\n```\n\n## API surface — Spatie parity\n\n| Spatie feature                                | URL emitted                                  | Builder method                                          |\n|-----------------------------------------------|----------------------------------------------|---------------------------------------------------------|\n| Partial / exact / scope / callback filter     | `?filter[name]=…`                            | `.filter(name, value)`                                  |\n| Comma-separated values                        | `?filter[name]=a,b`                          | `.filter(name, [a, b])`                                 |\n| Dynamic operator (`FilterOperator::DYNAMIC`)  | `?filter[salary]=>3000`                      | `.where(name, FilterOperator.GREATER_THAN, value)`      |\n| BelongsTo filter                              | `?filter[post]=1`                            | `.filter('post', value)`                                |\n| Trashed (SoftDeletes)                         | `?filter[trashed]=with` / `only`             | `.withTrashed()` / `.onlyTrashed()`                     |\n| Nullable filter (v7.0.1)                      | `?filter[deleted_at]=null` / `not-null`      | `.whereNull(name)` / `.whereNotNull(name)`              |\n| Custom array delimiter (v7.2.0)               | `?filter[id]=1\\|2\\|3`                        | `.delimiter('\\|')` / `setFilterDelimiter('\\|')`         |\n| Filter groups (v7.3.0 / [#1060](https://github.com/spatie/laravel-query-builder/pull/1060)) | `?filter[shorthand]=…`                       | `.filterGroup(shorthand, value)`                        |\n| Sort (multi-field, `-` for desc)              | `?sort=-created_at,name`                     | `.sortBy(field)` / `.sortByDesc(field)`                 |\n| Include relations                             | `?include=posts,profile`                     | `.include(...rels)`                                     |\n| Aggregate include `Count`                     | `?include=postsCount`                        | `.includeCount(...rels)`                                |\n| Aggregate include `Exists`                    | `?include=postsExists`                       | `.includeExists(...rels)`                               |\n| Aggregate include `sum / avg / min / max`     | `?include=postsViewsSum`                     | `.includeSum / Avg / Min / Max(...names)`               |\n| Sparse fieldsets                              | `?fields[users]=id,name`                     | `.fields(type, ...names)`                               |\n| Append accessors                              | `?append=full_name`                          | `.append(...names)`                                     |\n| Pagination (Laravel Resource)                 | `?page=2&per_page=25`                        | `.page(n)` / `.perPage(n)` / `.paginate(p, pp)`         |\n\n## Compatibility\n\n- **Node:** ≥ 18 (uses native `fetch`)\n- **Bundlers / frameworks:** Vite, Webpack 5+, Rollup, esbuild — Nuxt 3, Next.js 13+, Vue 3, React 18+, SvelteKit\n- **Backend:** Laravel + Spatie `laravel-query-builder` ≥ 7.0 (Filter Groups require ≥ 7.3.0)\n\n## Development\n\n```bash\nnpm install\nnpm run typecheck\nnpm test\nnpm run build\n```\n\n## Contributing\n\nIssues and pull requests welcome on [GitHub](https://github.com/taskinbirtan/crisp-oquent). For Spatie URL contract questions, please link to the relevant `laravel-query-builder` documentation or PR.\n\n## License\n\n[Apache-2.0](./LICENSE)\n","readmeFilename":"README.md","_rev":"1-3436aafc076c4d22228a7e3b4b645eb6"}