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 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
- ### `filter` / `sort` / `expand` — type-checked suggestions
249
+ ### Filtering, sorting & expanding
246
250
 
247
- With a typed service, the query options validate field names (and filter
248
- operators) at compile time — your editor suggests valid fields as you type:
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
- await postsSvc.getList(1, 20, { sort: '-title' }); // ✓ suggests title/published/…
252
- await postsSvc.getList(1, 20, { sort: '-nope' }); // ✗ compile error
253
- await postsSvc.getList(1, 20, { sort: 'title, nope' }); // ✗ every token checked
254
-
255
- await postsSvc.getList(1, 20, {
256
- filter: "title ~ 'x' && published = true", // ✓ every clause field + operator checked
257
- });
258
- await postsSvc.getList(1, 20, { filter: `title=${search}` }); // ✓ spaces optional
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
- await postsSvc.getList(1, 20, { expand: 'author' }); // ✓ field suggested
268
- await postsSvc.getList(1, 20, { expand: 'author.user' }); // ✓ nested dot-path
269
- await postsSvc.getList(1, 20, { expand: 'author, nope' }); // ✗ every token checked
270
- await postsSvc.getOne('abc', { expand: 'author' });
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
- // Expanded records carry an `expand` property keyed by the requested fields,
273
- // typed to the target collection's record (codegen client):
274
- const posts = await postsSvc.getFullList({ expand: 'author' });
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
- - `filter` — `field op value` clauses with `= != ~ !~ > >= < <=` operators,
279
- plus the PocketBase `?`-prefixed array operators `?= ?!= ?~ ?!~ ?> ?>= ?< ?<=`
280
- ("any/at-least-one-of" over multi-select / multiple relation / multiple file
281
- fields: `tags ?= 'news'`); spaces around the operator are optional; `&&`,
282
- `||`, `!`, and parentheses are allowed; relation dot-paths like
283
- `author.email = 'x'` typecheck. **Every** clause's field name and operator
284
- are validated — a typo in any clause is a compile error, and quoted values
285
- may contain `&&`/`||` (e.g. `title ~ 'a && b'`). Field names and operators
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
- const posts = await postsSvc.getFullList({ expand: 'author' });
303
- posts[0].expand?.author?.email; // ✓ typed UsersRecord (auth → has email)
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
- Multi-relations (`maxSelect > 1`) expand to `Array<TargetRecord>`, auth
307
- targets carry the `AuthRecord` fields, and unknown/unresolvable targets fall
308
- back to `unknown`. Hand-written services (no schema, e.g.
309
- `createClient<MyCollections>()`) keep `unknown` values.
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 (no `record.user`) but the server still resolves it for
313
- `filter`/`sort`/`expand`/`select` — the generated `*QueryFields` type keeps
314
- those keys accepted, so `getFullList({ expand: "user" })` typechecks for a
315
- hidden relation (and `record.expand?.user` is typed to the target record on
316
- the result).
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
- - `authMethods(options?)` — Get available auth methods
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