lazypock 0.13.0 → 0.14.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 +130 -60
- package/dist/index.cjs +464 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +206 -12
- package/dist/index.d.ts +206 -12
- package/dist/index.global.js +462 -40
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +462 -40
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/collection.ts +422 -40
- package/src/filter.ts +193 -0
- package/src/index.ts +3 -0
- package/src/lazypock.ts +12 -0
- package/src/types.ts +29 -10
package/README.md
CHANGED
|
@@ -246,78 +246,143 @@ const session = await client.authWithPassword('users', 'ada@example.com', 'corre
|
|
|
246
246
|
const whoami = await client.me(); // fresh record via GET /api/me
|
|
247
247
|
```
|
|
248
248
|
|
|
249
|
-
###
|
|
249
|
+
### Filtering, sorting & expanding
|
|
250
250
|
|
|
251
|
-
|
|
252
|
-
|
|
251
|
+
Typed services validate every `filter` / `sort` / `expand` field at compile
|
|
252
|
+
time. Use the **array** forms of `sort`/`expand` and the typed `where()`
|
|
253
|
+
builder to get **autocomplete** while you type.
|
|
253
254
|
|
|
254
255
|
```typescript
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
await postsSvc.getList(1, 20, { sort: '
|
|
258
|
-
|
|
259
|
-
await postsSvc.getList(1, 20, {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
await postsSvc.getList(1, 20, { filter: "(title = 'a' || title = 'b')" }); // ✓ parens
|
|
264
|
-
await postsSvc.getList(1, 20, { filter: "author.email = 'x'" }); // ✓ relation dot-path
|
|
265
|
-
await postsSvc.getList(1, 20, { filter: "tags ?= 'news'" }); // ✓ any/at-least-one-of
|
|
266
|
-
await postsSvc.getList(1, 20, { filter: 'nope = 1' }); // ✗ compile error
|
|
267
|
-
await postsSvc.getList(1, 20, {
|
|
268
|
-
filter: "title = 'a' && nope = 'y'", // ✗ EVERY clause is validated
|
|
256
|
+
const q = postsSvc.where; // typed filter builder
|
|
257
|
+
|
|
258
|
+
await postsSvc.getList(1, 20, { sort: ['-created'] }); // sort
|
|
259
|
+
await postsSvc.getFullList({ filter: q('id').in(ids) }); // filter by ids
|
|
260
|
+
await postsSvc.getList(1, 20, { filter: q('title').contains(term) }); // search
|
|
261
|
+
await postsSvc.getFullList({
|
|
262
|
+
filter: q('published').eq(true),
|
|
263
|
+
expand: ['author.name'],
|
|
269
264
|
});
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
| I want to… | Do this |
|
|
268
|
+
| --- | --- |
|
|
269
|
+
| Sort newest first | `getList(1, 20, { sort: ['-created'] })` |
|
|
270
|
+
| Filter by a list of ids | `getFullList({ filter: postsSvc.where('id').in(ids) })` |
|
|
271
|
+
| Search a text field | `getList(1, 20, { filter: postsSvc.where('title').contains(term) })` |
|
|
272
|
+
| Both conditions (AND) | `q('published').eq(true).and(q('views').gt(100))` |
|
|
273
|
+
| Either condition (OR) | `q('a').eq(1).or(q('b').eq(2))` |
|
|
274
|
+
| Negate a condition | `q('archived').eq(true).not()` |
|
|
275
|
+
| Filter by relation id | `q('author').eq(userId)` |
|
|
276
|
+
| Expand a relation | `getList(1, 20, { expand: ['author'] })` |
|
|
277
|
+
| Only some expanded fields | `getList(1, 20, { expand: ['author.name'] })` |
|
|
278
|
+
| Return only some fields | `client.collection('posts').select('id', 'title').getList()` |
|
|
279
|
+
|
|
280
|
+
#### Sorting
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
await postsSvc.getList(1, 20, { sort: '-created' }); // string
|
|
284
|
+
await postsSvc.getList(1, 20, { sort: ['title', '-published'] }); // array — suggests each field
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
`-field` = descending, `+field` / `field` = ascending. **Every** token is
|
|
288
|
+
validated (array and string alike); the array form adds per-field
|
|
289
|
+
autocomplete.
|
|
290
|
+
|
|
291
|
+
#### Filtering — string expressions
|
|
292
|
+
|
|
293
|
+
Full PocketBase syntax, validated clause by clause — a typo in any clause is
|
|
294
|
+
a compile error:
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
await postsSvc.getList(1, 20, { filter: "title ~ 'hello' && published = true" });
|
|
298
|
+
await postsSvc.getList(1, 20, { filter: "(title = 'a' || title = 'b')" });
|
|
299
|
+
await postsSvc.getList(1, 20, { filter: "tags ?= 'news'" }); // any array element
|
|
300
|
+
await postsSvc.getList(1, 20, { filter: "author.email = 'ada@example.com'" });
|
|
301
|
+
await postsSvc.getList(1, 20, { filter: "deleted_at = null" }); // IS NULL
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
> **Relation dot-paths** (`author.email = 'x'`, including multi-level paths)
|
|
305
|
+
> and `field = null` / `field != null` are supported by current LazyPock
|
|
306
|
+
> servers. On older servers a dot-path filter is rejected with HTTP `400` —
|
|
307
|
+
> filter by the relation id (`author = 'USER_ID'`) instead.
|
|
270
308
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
309
|
+
| Operator | Meaning |
|
|
310
|
+
| --- | --- |
|
|
311
|
+
| `=` `!=` | equal / not equal |
|
|
312
|
+
| `~` `!~` | contains / does not contain |
|
|
313
|
+
| `>` `>=` `<` `<=` | comparisons |
|
|
314
|
+
| `?=` `?!=` `?~` `?!~` `?>` `?>=` `?<` `?<=` | "any element matches" variants for array fields |
|
|
275
315
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
316
|
+
#### Filtering — typed builder
|
|
317
|
+
|
|
318
|
+
`service.where(field)` suggests field names, turns operators into methods, and
|
|
319
|
+
escapes values (no manual quoting):
|
|
320
|
+
|
|
321
|
+
```typescript
|
|
322
|
+
const q = postsSvc.where;
|
|
323
|
+
|
|
324
|
+
q('title').eq('x'); // title = 'x'
|
|
325
|
+
q('title').contains('x'); // title ~ 'x'
|
|
326
|
+
q('published').eq(true); // published = true
|
|
327
|
+
q('views').gte(100); // views >= 100
|
|
328
|
+
q('title').eq(null); // title = null (is empty)
|
|
329
|
+
q('id').in(['a', 'b', 'c']); // (id = 'a' || id = 'b' || id = 'c') — list membership
|
|
330
|
+
q('id').notIn(['a', 'b']); // (id != 'a' && id != 'b')
|
|
331
|
+
q('tags').anyEq('news'); // any array element equals
|
|
332
|
+
q('author').eq(userId); // filter by relation id
|
|
333
|
+
q('author.email').eq('ada@example.com'); // relation dot-path (null + != supported too)
|
|
334
|
+
|
|
335
|
+
// combine — and() / or() / not()
|
|
336
|
+
q('title').contains('x').and(q('published').eq(true)); // (… && …)
|
|
337
|
+
q('a').eq(1).or(q('b').eq(2)).not(); // !((… || …))
|
|
280
338
|
```
|
|
281
339
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
- `
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
typed as its **target collection's record** — the generated `*ExpandMap`
|
|
302
|
-
types resolve relation fields to their target record type, so
|
|
303
|
-
`record.expand.user` is `UsersRecord`, not `unknown`:
|
|
340
|
+
| Comparison | Emits | Array (`any…`) | Emits |
|
|
341
|
+
| --- | --- | --- | --- |
|
|
342
|
+
| `eq(v)` | `field = v` | `anyEq(v)` | `field ?= v` |
|
|
343
|
+
| `neq(v)` | `field != v` | `anyNeq(v)` | `field ?!= v` |
|
|
344
|
+
| `contains(v)` | `field ~ v` | `anyContains(v)` | `field ?~ v` |
|
|
345
|
+
| `notContains(v)` | `field !~ v` | `anyNotContains(v)` | `field ?!~ v` |
|
|
346
|
+
| `gt(v)` / `gte(v)` | `field > v` / `field >= v` | `anyGt(v)` / `anyGte(v)` | `field ?> v` / `field ?>= v` |
|
|
347
|
+
| `lt(v)` / `lte(v)` | `field < v` / `field <= v` | `anyLt(v)` / `anyLte(v)` | `field ?< v` / `field ?<= v` |
|
|
348
|
+
| `in(values)` | `(field = a \|\| field = b \|\| …)` | `notIn(values)` | `(field != a && field != b && …)` |
|
|
349
|
+
|
|
350
|
+
Values may be `string | number | boolean | null`; the server enforces the
|
|
351
|
+
per-field type. An empty `in([])` / `notIn([])` throws. A built expression
|
|
352
|
+
can be passed anywhere a `filter` string is accepted — including
|
|
353
|
+
`getFirstListItem(q('slug').eq('hello-world'))`.
|
|
354
|
+
|
|
355
|
+
#### Expanding relations
|
|
356
|
+
|
|
357
|
+
`expand` attaches the related record(s) under `record.expand`; the relation
|
|
358
|
+
field itself keeps the id:
|
|
304
359
|
|
|
305
360
|
```typescript
|
|
306
|
-
|
|
307
|
-
|
|
361
|
+
await postsSvc.getOne('abc', { expand: 'author' }); // full related record
|
|
362
|
+
await postsSvc.getFullList({ expand: ['author', 'category'] }); // array form
|
|
363
|
+
await postsSvc.getFullList({ expand: ['author.profile'] }); // nested relation
|
|
364
|
+
await postsSvc.getFullList({ expand: ['author.name', 'author.email'] }); // only some fields
|
|
308
365
|
```
|
|
309
366
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
367
|
+
- `record.expand.author` is the related record; `record.author` is the id.
|
|
368
|
+
- Field selection (`author.name`) is applied for you — PocketBase narrows it
|
|
369
|
+
server-side, the LazyPock server returns the full related record and the SDK
|
|
370
|
+
keeps only the requested fields.
|
|
371
|
+
- Distinguishing a field (`author.name`) from a nested relation
|
|
372
|
+
(`author.profile`) needs the target schema. Codegen's `createClient()` wires
|
|
373
|
+
it in automatically; with a hand-written client, pass `types.schemas`.
|
|
374
|
+
- On a typed service, `record.expand.author` is typed as the target record
|
|
375
|
+
(`record.expand.user` is `UsersRecord`, not `unknown`); multi-relations
|
|
376
|
+
(`maxSelect > 1`) become arrays.
|
|
377
|
+
- The relation field + `expand.*` entries are merged into any active
|
|
378
|
+
`select()` / `fields` projection, so the expanded data is never dropped.
|
|
314
379
|
|
|
315
380
|
**Hidden fields are queryable.** A hidden relation is excluded from the read
|
|
316
|
-
model
|
|
317
|
-
`
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
381
|
+
model but the server still resolves it for `filter` / `sort` / `expand` /
|
|
382
|
+
`select`.
|
|
383
|
+
|
|
384
|
+
> **Full guide with more recipes** (ids, search, relation filters, status
|
|
385
|
+
> queries): [Queries](https://lazypock.gnuzd.dev/sdk/typescript/queries).
|
|
321
386
|
|
|
322
387
|
- The **untyped** client (`client.collection('posts')` without `typed<T>()`)
|
|
323
388
|
still accepts any string — suggestions kick in once the service is typed.
|
|
@@ -391,11 +456,16 @@ PocketBase-style service for the collections themselves (admin):
|
|
|
391
456
|
|
|
392
457
|
Returned by `client.collection(name)`.
|
|
393
458
|
|
|
459
|
+
- `where(field)` — Start a **typed filter clause** (`where('id').in(ids)`, `where('title').contains(x)`);
|
|
460
|
+
field names are checked/suggested and values escaped — see [Filtering](#filtering-sorting--expanding)
|
|
394
461
|
- `select(...fields)` — Project reads to the given fields (see [Field projection](#field-projection-select--query-suggestions)); `select('*')` restores the all-visible default
|
|
395
|
-
- `getList(page, perPage, options?)` — Paginated list of records (typed `filter`/`sort`/`expand`/`fields`)
|
|
462
|
+
- `getList(page, perPage, options?)` — Paginated list of records (typed `filter`/`sort`/`expand`/`fields`; `sort`/`expand` accept a string or an array)
|
|
396
463
|
- `getFullList(options?)` — Fetch all records (auto-paginates)
|
|
397
|
-
- `getFirstListItem(filter, options?)` — Fetch first record matching filter
|
|
464
|
+
- `getFirstListItem(filter, options?)` — Fetch first record matching filter (`filter` may be a string or a `FilterExpr`)
|
|
398
465
|
- `getOne(id, options?)` — Get record by ID
|
|
466
|
+
- `expandFields(options?)` — List the collection's relation fields (for building `expand`)
|
|
467
|
+
- `typed<T>()` — Cast the service to a record shape (compile-time only)
|
|
468
|
+
- `withSchema(schema)` — Bind a schema explicitly (hidden-field exclusion + query checking)
|
|
399
469
|
- `create(data, options?)` — Create record
|
|
400
470
|
- `update(id, data, options?)` — Update record
|
|
401
471
|
- `delete(id, options?)` — Delete record
|