lazypock 0.12.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 +137 -61
- package/dist/index.cjs +763 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +353 -15
- package/dist/index.d.ts +353 -15
- package/dist/index.global.js +761 -45
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +760 -44
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/collection.ts +562 -57
- package/src/filter.ts +193 -0
- package/src/http.ts +7 -2
- package/src/index.ts +12 -0
- package/src/lazypock.ts +22 -0
- package/src/oauth2.ts +397 -0
- package/src/types.ts +44 -12
package/README.md
CHANGED
|
@@ -57,6 +57,10 @@ const user = await client.collection('users').create({
|
|
|
57
57
|
const session = await client.authWithPassword('users', 'ada@example.com', 'correct-horse-battery');
|
|
58
58
|
// session.token — stored in client.authStore for subsequent requests
|
|
59
59
|
|
|
60
|
+
// OAuth2 login — one call: opens the provider popup and stores the session
|
|
61
|
+
const oauth = await client.collection('users').authWithOAuth2({ provider: 'google' });
|
|
62
|
+
// oauth.token + oauth.record + oauth.meta (isNew, email, avatarURL, …)
|
|
63
|
+
|
|
60
64
|
// File upload
|
|
61
65
|
const file = await client.files.upload(fileInput.files[0]);
|
|
62
66
|
|
|
@@ -242,78 +246,143 @@ const session = await client.authWithPassword('users', 'ada@example.com', 'corre
|
|
|
242
246
|
const whoami = await client.me(); // fresh record via GET /api/me
|
|
243
247
|
```
|
|
244
248
|
|
|
245
|
-
###
|
|
249
|
+
### Filtering, sorting & expanding
|
|
246
250
|
|
|
247
|
-
|
|
248
|
-
|
|
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.
|
|
249
254
|
|
|
250
255
|
```typescript
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
await postsSvc.getList(1, 20, { sort: '
|
|
254
|
-
|
|
255
|
-
await postsSvc.getList(1, 20, {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
await postsSvc.getList(1, 20, { filter: "(title = 'a' || title = 'b')" }); // ✓ parens
|
|
260
|
-
await postsSvc.getList(1, 20, { filter: "author.email = 'x'" }); // ✓ relation dot-path
|
|
261
|
-
await postsSvc.getList(1, 20, { filter: "tags ?= 'news'" }); // ✓ any/at-least-one-of
|
|
262
|
-
await postsSvc.getList(1, 20, { filter: 'nope = 1' }); // ✗ compile error
|
|
263
|
-
await postsSvc.getList(1, 20, {
|
|
264
|
-
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'],
|
|
265
264
|
});
|
|
265
|
+
```
|
|
266
266
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
|
271
281
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
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
|
|
276
285
|
```
|
|
277
286
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
are suggested as you type.
|
|
287
|
-
- `sort` — `field`, `-field` (desc), `+field`, or comma-separated. **Every**
|
|
288
|
-
token is validated.
|
|
289
|
-
- `expand` — comma-separated relation field names, including nested dot-paths
|
|
290
|
-
(`author.user`); **every** token is validated; non-relation fields warn at
|
|
291
|
-
runtime when a schema is available.
|
|
292
|
-
|
|
293
|
-
**Expanded records are typed.** When a list/read is called with `expand`,
|
|
294
|
-
the returned records include an optional `expand` object whose keys are the
|
|
295
|
-
requested top-level relation fields (`record.expand.author` — dot-paths
|
|
296
|
-
collapse to their first segment). With the codegen client, each value is
|
|
297
|
-
typed as its **target collection's record** — the generated `*ExpandMap`
|
|
298
|
-
types resolve relation fields to their target record type, so
|
|
299
|
-
`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:
|
|
300
295
|
|
|
301
296
|
```typescript
|
|
302
|
-
|
|
303
|
-
|
|
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
|
|
304
302
|
```
|
|
305
303
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
+
#### Expanding relations
|
|
356
|
+
|
|
357
|
+
`expand` attaches the related record(s) under `record.expand`; the relation
|
|
358
|
+
field itself keeps the id:
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
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
|
|
365
|
+
```
|
|
366
|
+
|
|
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.
|
|
310
379
|
|
|
311
380
|
**Hidden fields are queryable.** A hidden relation is excluded from the read
|
|
312
|
-
model
|
|
313
|
-
`
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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).
|
|
317
386
|
|
|
318
387
|
- The **untyped** client (`client.collection('posts')` without `typed<T>()`)
|
|
319
388
|
still accepts any string — suggestions kick in once the service is typed.
|
|
@@ -387,11 +456,16 @@ PocketBase-style service for the collections themselves (admin):
|
|
|
387
456
|
|
|
388
457
|
Returned by `client.collection(name)`.
|
|
389
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)
|
|
390
461
|
- `select(...fields)` — Project reads to the given fields (see [Field projection](#field-projection-select--query-suggestions)); `select('*')` restores the all-visible default
|
|
391
|
-
- `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)
|
|
392
463
|
- `getFullList(options?)` — Fetch all records (auto-paginates)
|
|
393
|
-
- `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`)
|
|
394
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)
|
|
395
469
|
- `create(data, options?)` — Create record
|
|
396
470
|
- `update(id, data, options?)` — Update record
|
|
397
471
|
- `delete(id, options?)` — Delete record
|
|
@@ -399,7 +473,9 @@ Returned by `client.collection(name)`.
|
|
|
399
473
|
- `unsubscribe(topic?)` — `unsubscribe('*')` / `unsubscribe('id')` / `unsubscribe()` (all)
|
|
400
474
|
- `authWithPassword(identity, password, options?)` — Login to this auth collection
|
|
401
475
|
- `authRefresh(options?)` — Refresh token for this auth collection
|
|
402
|
-
- `
|
|
476
|
+
- `listAuthMethods(options?)` — Get available auth methods (password + OAuth2 providers)
|
|
477
|
+
- `authWithOAuth2(options)` — Sign in with an OAuth2 provider (popup flow)
|
|
478
|
+
- `authWithOAuth2Code(options)` — Exchange an OAuth2 authorization code directly
|
|
403
479
|
|
|
404
480
|
### AuthStore
|
|
405
481
|
|