ng-qubee 3.2.0 → 3.3.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
@@ -41,7 +41,7 @@ npm i ng-qubee
41
41
 
42
42
  ## Drivers
43
43
 
44
- NgQubee supports four drivers out of the box. A driver **must** be specified in the configuration:
44
+ NgQubee supports five drivers out of the box. A driver **must** be specified in the configuration:
45
45
 
46
46
  | Driver | Backend | Request Format | Response Format |
47
47
  |---|---|---|---|
@@ -49,6 +49,7 @@ NgQubee supports four drivers out of the box. A driver **must** be specified in
49
49
  | **Laravel** | Plain Laravel pagination | `limit=N&page=N` (pagination only) | Flat: `{ data, current_page, total, ... }` |
50
50
  | **Spatie** | Spatie Query Builder | `filter[field]=value`, `sort=-field` | Flat: `{ data, current_page, total, ... }` |
51
51
  | **NestJS** | nestjs-paginate | `filter.field=$operator:value`, `sortBy=field:DESC` | Nested: `{ data, meta: {...}, links: {...} }` |
52
+ | **PostgREST** | PostgREST / Supabase | `col=eq.value`, `order=col.asc`, `limit=N&offset=M` | Bare array body + `Content-Range` header for total |
52
53
 
53
54
  ## Usage
54
55
 
@@ -176,6 +177,120 @@ The NestJS driver generates URIs compatible with [nestjs-paginate](https://githu
176
177
  - **Limit** is composed as `limit=15`
177
178
  - **Page** is composed as `page=1`
178
179
 
180
+ ### PostgREST / Supabase Driver
181
+
182
+ The PostgREST driver generates URIs compatible with [PostgREST](https://postgrest.org/) and anything built on top of it (notably [Supabase](https://supabase.com/)):
183
+
184
+ ```typescript
185
+ import { DriverEnum } from 'ng-qubee';
186
+
187
+ // Standalone approach
188
+ bootstrapApplication(AppComponent, {
189
+ providers: [provideNgQubee({ driver: DriverEnum.POSTGREST })]
190
+ });
191
+
192
+ // Module approach
193
+ @NgModule({
194
+ imports: [
195
+ NgQubeeModule.forRoot({ driver: DriverEnum.POSTGREST })
196
+ ]
197
+ })
198
+ export class AppModule {}
199
+ ```
200
+
201
+ The PostgREST driver supports:
202
+
203
+ - **Filters** (single value) are composed as `col=eq.value`
204
+ - **Filters** (multi-value) are composed as `col=in.(v1,v2,v3)` — PostgREST's native IN-list syntax
205
+ - **Operator filters** cover the full `FilterOperatorEnum` (eq, gt, gte, lt, lte, ilike, in, not, null, btw, sw, contains) plus PostgREST-native full-text search (`fts`, `plfts`, `phfts`, `wfts`) — see the Operator filters section below
206
+ - **Sorts** are composed as `order=col1.asc,col2.desc`
207
+ - **Select** is composed as `select=col1,col2`
208
+ - **Pagination** defaults to `limit=N&offset=M` on the URL and can optionally move to `Range-Unit` + `Range` HTTP headers — see the RANGE-header pagination section below
209
+
210
+ #### Reading totals from the `Content-Range` header
211
+
212
+ PostgREST returns a bare array body and reports the total row count in the `Content-Range` HTTP response header (e.g. `0-9/50`). Opt in by sending `Prefer: count=exact` on the request, then pass the headers to `paginate()`:
213
+
214
+ ```typescript
215
+ this._http.get<User[]>(uri, {
216
+ observe: 'response',
217
+ headers: { 'Prefer': 'count=exact' }
218
+ }).subscribe(response => {
219
+ const collection = this._paginationService.paginate(response.body, response.headers);
220
+ // collection.total, collection.page, collection.lastPage are populated
221
+ });
222
+ ```
223
+
224
+ The second argument to `paginate()` accepts Angular's `HttpHeaders`, the native `Headers` class, or a plain `Record<string, string>` — whatever shape your HTTP client emits. If you omit the header (or the server returns `Content-Range: 0-9/*`), the collection still populates `data` and `page` but leaves `total` / `lastPage` undefined — the pagination helpers' conservative defaults then kick in (`hasNextPage()` returns `true`, `isLastPage()` returns `false`).
225
+
226
+ #### Operator filters
227
+
228
+ Every `FilterOperatorEnum` value maps to a PostgREST prefix operator. Call `addFilterOperator(column, operator, ...values)` and the strategy emits the correct wire format:
229
+
230
+ ```typescript
231
+ qb.addFilterOperator('age', FilterOperatorEnum.GTE, 18); // age=gte.18
232
+ qb.addFilterOperator('id', FilterOperatorEnum.IN, 1, 2, 3); // id=in.(1,2,3)
233
+ qb.addFilterOperator('status', FilterOperatorEnum.NOT, 'deleted'); // status=not.eq.deleted
234
+ qb.addFilterOperator('id', FilterOperatorEnum.NOT, 1, 2); // id=not.in.(1,2)
235
+ qb.addFilterOperator('deletedAt', FilterOperatorEnum.NULL, true); // deletedAt=is.null
236
+ qb.addFilterOperator('deletedAt', FilterOperatorEnum.NULL, false); // deletedAt=is.not.null
237
+ qb.addFilterOperator('price', FilterOperatorEnum.BTW, 10, 100); // price=gte.10&price=lte.100
238
+ qb.addFilterOperator('email', FilterOperatorEnum.SW, 'admin'); // email=like.admin*
239
+ qb.addFilterOperator('name', FilterOperatorEnum.CONTAINS, 'john'); // name=ilike.%john%
240
+ qb.addFilterOperator('description', FilterOperatorEnum.FTS, 'rat'); // description=fts.rat
241
+ qb.addFilterOperator('description', FilterOperatorEnum.PLFTS, 'a b'); // description=plfts.a b
242
+ qb.addFilterOperator('description', FilterOperatorEnum.PHFTS, 'a b'); // description=phfts.a b
243
+ qb.addFilterOperator('description', FilterOperatorEnum.WFTS, 'a -b'); // description=wfts.a -b
244
+ ```
245
+
246
+ Value shape rules enforced at call time (throw `InvalidFilterOperatorValueError`):
247
+ - `BTW` — exactly 2 values (`[min, max]`).
248
+ - `NULL` — exactly 1 boolean value (`true` → `IS NULL`, `false` → `IS NOT NULL`).
249
+
250
+ All other operators let PostgREST validate server-side.
251
+
252
+ The four `*FTS` operators are PostgREST's full-text search variants (`to_tsquery`, `plainto_tsquery`, `phraseto_tsquery`, `websearch_to_tsquery`). They're column-scoped — pick the column to search, pass the term. Language modifiers (`fts(english).term`) are not supported in this release.
253
+
254
+ #### RANGE-header pagination (alternative to limit/offset)
255
+
256
+ PostgREST also accepts pagination via the `Range` HTTP request header instead of URL query params. Opt in via `IConfig.pagination`:
257
+
258
+ ```typescript
259
+ import { DriverEnum, PaginationModeEnum } from 'ng-qubee';
260
+
261
+ provideNgQubee({
262
+ driver: DriverEnum.POSTGREST,
263
+ pagination: PaginationModeEnum.RANGE
264
+ });
265
+ ```
266
+
267
+ When `RANGE` is active, `generateUri()` omits `limit` and `offset` from the URL and `NgQubeeService.paginationHeaders()` returns the headers the consumer applies to the HTTP request:
268
+
269
+ ```typescript
270
+ const uri = await firstValueFrom(qb.generateUri());
271
+ const extraHeaders = qb.paginationHeaders(); // { 'Range-Unit': 'items', 'Range': '0-9' }
272
+
273
+ this._http.get<User[]>(uri, {
274
+ observe: 'response',
275
+ headers: { 'Prefer': 'count=exact', ...extraHeaders }
276
+ }).subscribe(resp => this._pagination.paginate(resp.body, resp.headers));
277
+ ```
278
+
279
+ `paginationHeaders()` returns `null` for any driver that doesn't use header-based pagination — safe to spread into a headers map unconditionally with the nullish-coalescing pattern above.
280
+
281
+ #### Feature matrix
282
+
283
+ | Method | Supported? | Notes |
284
+ |---|---|---|
285
+ | `addFilter` / `deleteFilters` | ✓ | Implicit `eq`; multi-value becomes `in.(...)` |
286
+ | `addFilterOperator` / `deleteOperatorFilters` | ✓ | All 16 operators including `FTS`/`PLFTS`/`PHFTS`/`WFTS` |
287
+ | `addSort` / `deleteSorts` | ✓ | Emits `order=col.asc,col.desc` |
288
+ | `addSelect` / `deleteSelect` | ✓ | Flat column selection |
289
+ | `setLimit` / `setPage` | ✓ | `offset` derived from `page` (QUERY mode) or emitted via `Range` header (RANGE mode) |
290
+ | `addFields` / `deleteFields` / `deleteFieldsByModel` | ✗ | Throws `UnsupportedFieldSelectionError`. Per-type field selection is a JSON:API/Spatie concept; use `addSelect` for PostgREST's column pruning. |
291
+ | `addIncludes` / `deleteIncludes` | ✗ | Throws `UnsupportedIncludesError`. PostgREST uses embedded resources via `select=col,rel(*)` — tracked as #66. |
292
+ | `setSearch` / `deleteSearch` | ✗ | Throws `UnsupportedSearchError`. PostgREST's FTS operators are per-column — use `addFilterOperator(col, FilterOperatorEnum.FTS, term)` instead. |
293
+
179
294
  ### Per-component instances
180
295
 
181
296
  By default, `provideNgQubee()` / `NgQubeeModule.forRoot()` register `NgQubeeService` at the environment injector, so every component that injects it shares the same query-builder state. If you need a dedicated instance — e.g. a feature component whose filters and pagination must not bleed into the app-wide one — spread `provideNgQubeeInstance()` into the component's `providers`: