lazypock 0.13.0 → 0.15.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 +139 -60
- package/dist/index.cjs +483 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +235 -14
- package/dist/index.d.ts +235 -14
- package/dist/index.global.js +481 -40
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +481 -40
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/collection.ts +450 -41
- package/src/filter.ts +193 -0
- package/src/index.ts +5 -0
- package/src/lazypock.ts +16 -0
- package/src/types.ts +59 -12
package/README.md
CHANGED
|
@@ -246,78 +246,152 @@ 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
|
+
```
|
|
270
266
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
|
275
281
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
posts[0].expand?.author?.email; // ✓ typed, not unknown
|
|
282
|
+
```typescript
|
|
283
|
+
await postsSvc.getList(1, 20, { sort: '-created' }); // string
|
|
284
|
+
await postsSvc.getList(1, 20, { sort: ['title', '-published'] }); // array — suggests each field
|
|
280
285
|
```
|
|
281
286
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
are suggested as you type.
|
|
291
|
-
- `sort` — `field`, `-field` (desc), `+field`, or comma-separated. **Every**
|
|
292
|
-
token is validated.
|
|
293
|
-
- `expand` — comma-separated relation field names, including nested dot-paths
|
|
294
|
-
(`author.user`); **every** token is validated; non-relation fields warn at
|
|
295
|
-
runtime when a schema is available.
|
|
296
|
-
|
|
297
|
-
**Expanded records are typed.** When a list/read is called with `expand`,
|
|
298
|
-
the returned records include an optional `expand` object whose keys are the
|
|
299
|
-
requested top-level relation fields (`record.expand.author` — dot-paths
|
|
300
|
-
collapse to their first segment). With the codegen client, each value is
|
|
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`:
|
|
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:
|
|
304
295
|
|
|
305
296
|
```typescript
|
|
306
|
-
|
|
307
|
-
|
|
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
|
|
308
302
|
```
|
|
309
303
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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.
|
|
308
|
+
|
|
309
|
+
| Operator | Meaning |
|
|
310
|
+
| --- | --- |
|
|
311
|
+
| `=` `!=` | equal / not equal |
|
|
312
|
+
| `~` `!~` | contains / does not contain |
|
|
313
|
+
| `>` `>=` `<` `<=` | comparisons |
|
|
314
|
+
| `?=` `?!=` `?~` `?!~` `?>` `?>=` `?<` `?<=` | "any element matches" variants for array fields |
|
|
315
|
+
|
|
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(); // !((… || …))
|
|
338
|
+
```
|
|
339
|
+
|
|
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
|
+
For a one-off filter, build inline with a callback — it receives the same
|
|
356
|
+
typed `where` helper, so field names are still suggested and checked:
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
await postsSvc.getFullList({
|
|
360
|
+
filter: (w) => w('title').contains('x').and(w('published').eq(true)),
|
|
361
|
+
});
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
#### Expanding relations
|
|
365
|
+
|
|
366
|
+
`expand` attaches the related record(s) under `record.expand`; the relation
|
|
367
|
+
field itself keeps the id:
|
|
368
|
+
|
|
369
|
+
```typescript
|
|
370
|
+
await postsSvc.getOne('abc', { expand: 'author' }); // full related record
|
|
371
|
+
await postsSvc.getFullList({ expand: ['author', 'category'] }); // array form
|
|
372
|
+
await postsSvc.getFullList({ expand: ['author.profile'] }); // nested relation
|
|
373
|
+
await postsSvc.getFullList({ expand: ['author.name', 'author.email'] }); // only some fields
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
- `record.expand.author` is the related record; `record.author` is the id.
|
|
377
|
+
- Field selection (`author.name`) is applied for you — PocketBase narrows it
|
|
378
|
+
server-side, the LazyPock server returns the full related record and the SDK
|
|
379
|
+
keeps only the requested fields.
|
|
380
|
+
- Distinguishing a field (`author.name`) from a nested relation
|
|
381
|
+
(`author.profile`) needs the target schema. Codegen's `createClient()` wires
|
|
382
|
+
it in automatically; with a hand-written client, pass `types.schemas`.
|
|
383
|
+
- On a typed service, `record.expand.author` is typed as the target record
|
|
384
|
+
(`record.expand.user` is `UsersRecord`, not `unknown`); multi-relations
|
|
385
|
+
(`maxSelect > 1`) become arrays.
|
|
386
|
+
- The relation field + `expand.*` entries are merged into any active
|
|
387
|
+
`select()` / `fields` projection, so the expanded data is never dropped.
|
|
314
388
|
|
|
315
389
|
**Hidden fields are queryable.** A hidden relation is excluded from the read
|
|
316
|
-
model
|
|
317
|
-
`
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
390
|
+
model but the server still resolves it for `filter` / `sort` / `expand` /
|
|
391
|
+
`select`.
|
|
392
|
+
|
|
393
|
+
> **Full guide with more recipes** (ids, search, relation filters, status
|
|
394
|
+
> queries): [Queries](https://lazypock.gnuzd.dev/sdk/typescript/queries).
|
|
321
395
|
|
|
322
396
|
- The **untyped** client (`client.collection('posts')` without `typed<T>()`)
|
|
323
397
|
still accepts any string — suggestions kick in once the service is typed.
|
|
@@ -391,11 +465,16 @@ PocketBase-style service for the collections themselves (admin):
|
|
|
391
465
|
|
|
392
466
|
Returned by `client.collection(name)`.
|
|
393
467
|
|
|
468
|
+
- `where(field)` — Start a **typed filter clause** (`where('id').in(ids)`, `where('title').contains(x)`);
|
|
469
|
+
field names are checked/suggested and values escaped — see [Filtering](#filtering-sorting--expanding)
|
|
394
470
|
- `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`)
|
|
471
|
+
- `getList(page, perPage, options?)` — Paginated list of records (typed `filter`/`sort`/`expand`/`fields`; `sort`/`expand` accept a string or an array)
|
|
396
472
|
- `getFullList(options?)` — Fetch all records (auto-paginates)
|
|
397
|
-
- `getFirstListItem(filter, options?)` — Fetch first record matching filter
|
|
473
|
+
- `getFirstListItem(filter, options?)` — Fetch first record matching filter (`filter` may be a string or a `FilterExpr`)
|
|
398
474
|
- `getOne(id, options?)` — Get record by ID
|
|
475
|
+
- `expandFields(options?)` — List the collection's relation fields (for building `expand`)
|
|
476
|
+
- `typed<T>()` — Cast the service to a record shape (compile-time only)
|
|
477
|
+
- `withSchema(schema)` — Bind a schema explicitly (hidden-field exclusion + query checking)
|
|
399
478
|
- `create(data, options?)` — Create record
|
|
400
479
|
- `update(id, data, options?)` — Update record
|
|
401
480
|
- `delete(id, options?)` — Delete record
|