lazypock 0.10.2 → 0.12.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
@@ -1,6 +1,13 @@
1
- # Lazypock — TypeScript SDK
1
+ # lazypock
2
2
 
3
- TypeScript client library for [Lazypock](https://github.com/gnuzd/lazypock), an open-source PocketBase-compatible backend.
3
+ [![npm version](https://img.shields.io/npm/v/lazypock.svg)](https://www.npmjs.com/package/lazypock)
4
+ [![npm downloads](https://img.shields.io/npm/dm/lazypock.svg)](https://www.npmjs.com/package/lazypock)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/gnuzd/lazypock-ts/blob/main/LICENSE)
6
+ [![Backend: lazypock](https://img.shields.io/badge/backend-gnuzd%2Flazypock-2f5233)](https://github.com/gnuzd/lazypock)
7
+
8
+ TypeScript client SDK for **[Lazypock](https://github.com/gnuzd/lazypock)**, an open-source, PocketBase-compatible backend built on Elixir + Phoenix + PostgreSQL.
9
+
10
+ Full SDK docs (codegen, type safety, queries, realtime, files, auth): **[lazypock.gnuzd.dev/sdk/typescript](https://lazypock.gnuzd.dev/sdk/typescript)**.
4
11
 
5
12
  ## Installation
6
13
 
@@ -8,6 +15,17 @@ TypeScript client library for [Lazypock](https://github.com/gnuzd/lazypock), an
8
15
  npm install lazypock
9
16
  ```
10
17
 
18
+ bun / pnpm / yarn work the same way.
19
+
20
+ > **Needs a running Lazypock server.** No server yet? The quickest way is:
21
+ >
22
+ > ```bash
23
+ > git clone git@github.com:gnuzd/lazypock.git && cd lazypock
24
+ > docker compose up --build
25
+ > ```
26
+ >
27
+ > Studio (admin UI) + REST API will be up at `http://localhost:4000`. See the [backend README](https://github.com/gnuzd/lazypock#try-it-in-60-seconds-docker) for details, prebuilt binaries, and building from source.
28
+
11
29
  ## Quick Start
12
30
 
13
31
  ```typescript
@@ -52,37 +70,26 @@ The SDK offers three levels of type safety — pick what fits your project.
52
70
 
53
71
  ### 1. Fully typed via codegen (recommended)
54
72
 
55
- Connect to your API once and generate a typed client — every collection becomes
56
- an interface with the exact field types from your schema (selects become string
57
- unions, relations become record IDs, etc.).
73
+ Connect to your API once and generate a typed client — every collection becomes an interface with the exact field types from your schema (selects become string unions, relations become record IDs, etc.).
58
74
 
59
75
  ```bash
60
- # In your app, after installing lazypock:
61
- npx lazypock-gen \
76
+ npx lazypock \
62
77
  --url http://localhost:4000/api \
63
78
  --email admin@example.com \
64
79
  --password your-password
65
80
  # writes ./lazypock.types.ts
66
81
  ```
67
82
 
68
- > `lazypock-gen` remains as a deprecated alias for backwards compatibility —
69
- > the canonical command is now simply `lazypock`:
70
- >
71
- > ```bash
72
- > npx lazypock --url http://localhost:4000/api --email admin@example.com --password your-password
73
- > ```
74
- >
75
- > **Use an API key instead of a password** (recommended). Generate one from the
76
- > Studio **Settings → API Keys** dashboard, then:
77
- >
78
- > ```bash
79
- > npx lazypock --url http://localhost:4000/api --apikey lazypock_xxxxxxxx
80
- > # or via env: LAZYPOCK_URL=... LAZYPOCK_API_KEY=... npx lazypock
81
- > ```
82
- >
83
- > API keys are stored as a SHA-256 hash (raw value shown once at generation) and
84
- > are scoped to collection listing — ideal for codegen (they can `GET /collections`
85
- > without a login round-trip, and cannot read or mutate your records).
83
+ > `lazypock-gen` remains as a deprecated alias for backwards compatibility — the canonical command is now simply `lazypock`.
84
+
85
+ **Use an API key instead of a password** (recommended). Generate one from Studio **Settings → API Keys**, then:
86
+
87
+ ```bash
88
+ npx lazypock --url http://localhost:4000/api --apikey lazypock_xxxxxxxx
89
+ # or via env: LAZYPOCK_URL=... LAZYPOCK_API_KEY=... npx lazypock
90
+ ```
91
+
92
+ API keys are stored as a SHA-256 hash (raw value shown once at generation) and are scoped to collection listing — ideal for codegen (they can `GET /collections` without a login round-trip, and cannot read or mutate your records).
86
93
 
87
94
  Then in your app:
88
95
 
@@ -100,13 +107,11 @@ await client.collection('posts').create({ title: 'x' }); // ✓
100
107
  await client.collection('posts').create({ nope: 1 }); // ✗ compile error
101
108
  ```
102
109
 
103
- > **Dynamic collection names are fully supported.** The typed client accepts any
104
- > runtime string for `collection(name)` and still returns the typed service for
105
- > known collection names. So route params and dynamic lookups work naturally:
110
+ > **Dynamic collection names are fully supported.** The typed client accepts any runtime string for `collection(name)` and still returns the typed service for known collection names — route params and dynamic lookups work naturally:
106
111
  >
107
112
  > ```typescript
108
113
  > function load(name: string) {
109
- > return client.collection(name).getList(); // ✓ works for any string
114
+ > return client.collection(name).getList(); // ✓ works for any string
110
115
  > }
111
116
  > ```
112
117
 
@@ -146,16 +151,35 @@ const client = new LazypockClient({
146
151
  const code = client.generateTypes(); // string — write to lazypock.types.ts
147
152
  ```
148
153
 
149
- > The codegen CLI emits a `lazypockSchema` snapshot next to the types, and the
150
- > generated `createClient()` wires it in automatically — so the schema-driven
151
- > behaviour below (hidden-field exclusion, query validation) works out of the box.
154
+ The codegen CLI emits a `lazypockSchema` snapshot next to the types, and the generated `createClient()` wires it in automatically — so the schema-driven behaviour below (hidden-field exclusion, query validation) works out of the box.
155
+
156
+ ### CLI reference
157
+
158
+ ```
159
+ lazypock [options]
160
+
161
+ Options:
162
+ --url <url> API base URL (or LAZYPOCK_URL)
163
+ --apikey <key> API key (or LAZYPOCK_API_KEY) — recommended, no login round-trip
164
+ --api-key <key> Deprecated alias for --apikey
165
+ --email <email> Superuser email (or LAZYPOCK_EMAIL)
166
+ --password <pw> Superuser password (or LAZYPOCK_PASSWORD)
167
+ --output <file> Output file (default: lazypock.types.ts)
168
+ --out <file> Deprecated alias for --output
169
+ --package <name> Package name to import (default: lazypock)
170
+ --skip-system Skip system collections
171
+ ```
172
+
173
+ You must provide credentials one of two ways (or via the matching env vars):
174
+
175
+ 1. `--apikey` / `LAZYPOCK_API_KEY` — scoped to collection listing, no login.
176
+ 2. `--email` + `--password` / matching env vars — superuser login.
152
177
 
153
- ### Field projection (`select`) & query suggestions
178
+ ## Field Projection (`select`) & Query Suggestions
154
179
 
155
- #### `.select(...)` — pick the fields you want
180
+ ### `select(...)` — pick the fields you want
156
181
 
157
- `select()` projects list/read responses to the given fields (PocketBase `fields`
158
- param). Field names are **type-checked** when the service is typed:
182
+ `select()` projects list/read responses to the given fields (PocketBase `fields` param). Field names are **type-checked** when the service is typed:
159
183
 
160
184
  ```typescript
161
185
  const t = await client.collection('posts').select('id', 'title').getList();
@@ -164,16 +188,12 @@ const t = await client.collection('posts').select('id', 'title').getList();
164
188
  await client.collection('posts').select('id', 'title').getOne('abc123'); // same
165
189
  ```
166
190
 
167
- - `select('*')` (or no `select()` call) — request **all visible fields**; hidden
168
- fields are excluded automatically when a schema is available.
191
+ - `select('*')` (or no `select()` call) — request all visible fields; hidden fields are excluded automatically when a schema is available.
169
192
  - `select()` with no arguments resets back to the default.
170
- - `select()` returns a **derived service** — the original is untouched, so you
171
- can keep one default service and project per-request.
193
+ - `select()` returns a **derived service** — the original is untouched, so you can keep one default service and project per-request.
172
194
  - Passing an explicit `fields` option overrides the `select()` preset.
173
195
 
174
- When a schema is known (via `types.schemas` or codegen), hidden fields are
175
- **not returned by the server**: every read sends `fields=<visible fields>` by
176
- default, and selecting an unknown field logs a warning.
196
+ When a schema is known (via `types.schemas` or codegen), hidden fields are **not returned by the server**: every read sends `fields=<visible fields>` by default, and selecting an unknown field logs a warning.
177
197
 
178
198
  #### Creating records in auth collections (write-only `password`)
179
199
 
@@ -222,7 +242,7 @@ const session = await client.authWithPassword('users', 'ada@example.com', 'corre
222
242
  const whoami = await client.me(); // fresh record via GET /api/me
223
243
  ```
224
244
 
225
- #### `filter` / `sort` / `expand` — type-checked suggestions
245
+ ### `filter` / `sort` / `expand` — type-checked suggestions
226
246
 
227
247
  With a typed service, the query options validate field names (and filter
228
248
  operators) at compile time — your editor suggests valid fields as you type:
@@ -238,6 +258,7 @@ await postsSvc.getList(1, 20, {
238
258
  await postsSvc.getList(1, 20, { filter: `title=${search}` }); // ✓ spaces optional
239
259
  await postsSvc.getList(1, 20, { filter: "(title = 'a' || title = 'b')" }); // ✓ parens
240
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
241
262
  await postsSvc.getList(1, 20, { filter: 'nope = 1' }); // ✗ compile error
242
263
  await postsSvc.getList(1, 20, {
243
264
  filter: "title = 'a' && nope = 'y'", // ✗ EVERY clause is validated
@@ -248,17 +269,21 @@ await postsSvc.getList(1, 20, { expand: 'author.user' }); // ✓ nested dot-path
248
269
  await postsSvc.getList(1, 20, { expand: 'author, nope' }); // ✗ every token checked
249
270
  await postsSvc.getOne('abc', { expand: 'author' });
250
271
 
251
- // Expanded records carry an `expand` property keyed by the requested fields:
272
+ // Expanded records carry an `expand` property keyed by the requested fields,
273
+ // typed to the target collection's record (codegen client):
252
274
  const posts = await postsSvc.getFullList({ expand: 'author' });
253
- posts[0].expand?.author; // ✓ the expanded relation (shape is `unknown`)
275
+ posts[0].expand?.author?.email; // ✓ typed, not unknown
254
276
  ```
255
277
 
256
- - `filter` — `field op value` clauses with `= != ~ !~ > >= < <=` operators
257
- (spaces around the operator optional); `&&`, `||`, `!`, and parentheses are
258
- allowed; relation dot-paths like `author.email = 'x'` typecheck. **Every**
259
- clause's field name and operator are validated — a typo in any clause is a
260
- compile error, and quoted values may contain `&&`/`||` (e.g.
261
- `title ~ 'a && b'`). Field names and operators are suggested as you type.
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.
262
287
  - `sort` — `field`, `-field` (desc), `+field`, or comma-separated. **Every**
263
288
  token is validated.
264
289
  - `expand` — comma-separated relation field names, including nested dot-paths
@@ -268,66 +293,46 @@ posts[0].expand?.author; // ✓ the expanded relation (shape is `unknown`)
268
293
  **Expanded records are typed.** When a list/read is called with `expand`,
269
294
  the returned records include an optional `expand` object whose keys are the
270
295
  requested top-level relation fields (`record.expand.author` — dot-paths
271
- collapse to their first segment). The expanded value's shape belongs to the
272
- target collection, so it is typed `unknown`.
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`:
300
+
301
+ ```typescript
302
+ const posts = await postsSvc.getFullList({ expand: 'author' });
303
+ posts[0].expand?.author?.email; // ✓ typed UsersRecord (auth → has email)
304
+ ```
305
+
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.
273
310
 
274
311
  **Hidden fields are queryable.** A hidden relation is excluded from the read
275
312
  model (no `record.user`) but the server still resolves it for
276
313
  `filter`/`sort`/`expand`/`select` — the generated `*QueryFields` type keeps
277
314
  those keys accepted, so `getFullList({ expand: "user" })` typechecks for a
278
- hidden relation (and `record.expand?.user` is typed on the result).
315
+ hidden relation (and `record.expand?.user` is typed to the target record on
316
+ the result).
279
317
 
280
318
  - The **untyped** client (`client.collection('posts')` without `typed<T>()`)
281
319
  still accepts any string — suggestions kick in once the service is typed.
282
320
 
283
- ### CLI reference
284
-
285
- ```bash
286
- lazypock [options]
287
-
288
- Options:
289
- --url <url> API base URL (or LAZYPOCK_URL)
290
- --apikey <key> API key (or LAZYPOCK_API_KEY) — recommended, no login round-trip
291
- --api-key <key> Deprecated alias for --apikey
292
- --email <email> Superuser email (or LAZYPOCK_EMAIL)
293
- --password <pw> Superuser password (or LAZYPOCK_PASSWORD)
294
- --output <file> Output file (default: lazypock.types.ts)
295
- --out <file> Deprecated alias for --output
296
- --package <name> Package name to import (default: lazypock)
297
- --skip-system Skip system collections
298
- ```
299
-
300
- > **Note:** `lazypock-gen` is still available as a deprecated alias.
301
-
302
- You must provide credentials one of two ways (or via the matching env vars):
303
-
304
- 1. `--apikey` / `LAZYPOCK_API_KEY` — scoped to collection listing, no login.
305
- 2. `--email` + `--password` / `LAZYPOCK_EMAIL` + `LAZYPOCK_PASSWORD` — superuser login.
306
-
307
- ```
308
-
309
-
310
321
  ## API Reference
311
322
 
312
- ### LazypockClient
323
+ ### `LazypockClient`
313
324
 
314
325
  The main client class.
315
326
 
316
327
  #### Constructor Options
317
328
 
318
329
  | Option | Type | Default | Description |
319
- |--------|------|---------|-------------|
330
+ | --- | --- | --- | --- |
320
331
  | `baseUrl` | `string` | required | API base URL (e.g. `http://localhost:4000/api`) |
321
332
  | `storage` | `StorageAdapter` | `memoryStorage` | Custom storage adapter for token persistence |
322
333
  | `authStore` | `AuthStore` | auto-created | Explicit auth store instance |
323
334
  | `realtime` | `RealtimeService` | auto-created | Real-time service for WebSocket subscriptions |
324
335
 
325
- #### Auto-Cancellation Methods
326
-
327
- - `autoCancellation(enable)` — Globally enable/disable auto-cancellation of duplicated pending requests
328
- - `cancelRequest(requestKey)` — Abort a single pending request by key (default `HTTP_METHOD + path`)
329
- - `cancelAllRequests()` — Abort all pending requests
330
-
331
336
  #### Authentication Methods
332
337
 
333
338
  - `login(email, password, collection?)` — Login as superuser or auth collection user
@@ -338,7 +343,13 @@ The main client class.
338
343
  - `logout()` — Clear auth state
339
344
  - `me(options?)` — Get current superuser profile
340
345
 
341
- #### Collections Service (`client.collections`)
346
+ #### Auto-Cancellation Methods
347
+
348
+ - `autoCancellation(enable)` — Globally enable/disable auto-cancellation of duplicated pending requests
349
+ - `cancelRequest(requestKey)` — Abort a single pending request by key (default `HTTP_METHOD + path`)
350
+ - `cancelAllRequests()` — Abort all pending requests
351
+
352
+ ### Collections Service (`client.collections`)
342
353
 
343
354
  PocketBase-style service for the collections themselves (admin):
344
355
 
@@ -350,14 +361,17 @@ PocketBase-style service for the collections themselves (admin):
350
361
  - `collections.delete(id, options?)` — Delete collection
351
362
  - `collections.subscribe(cb)` — Subscribe to collection create/update/delete events (returns unsubscribe fn)
352
363
  - `collections.unsubscribe()` — Unsubscribe from registry events
353
- #### File Operations
364
+
365
+ ### File Operations
354
366
 
355
367
  - `files.upload(file, filename?, options?, meta?)` — Upload a file
356
368
  - `files.getUrl(fileId)` — Get file metadata
357
369
  - `files.delete(fileId, options?)` — Delete a file
358
370
  - `getFileUrl(baseUrl, fileId)` — Construct a file URL from base URL and file ID (utility)
371
+ - `getThumbUrl(baseUrl, fileId, size)` — Construct a thumbnail URL for a pre-configured thumb size
372
+ - `getScaleUrl(baseUrl, fileId, size)` — Construct an on-demand scaled image URL (e.g. `100x100`, `400x`)
359
373
 
360
- #### Realtime
374
+ ### Realtime
361
375
 
362
376
  - `realtime.connect(opts)` — Connect to WebSocket
363
377
  - `realtime.disconnect()` — Disconnect
@@ -445,10 +459,7 @@ interface RequestOptions {
445
459
 
446
460
  ## Auto Cancellation
447
461
 
448
- The SDK auto-cancels duplicated pending requests for you (PocketBase-compatible
449
- behaviour). When a new request is issued with the same request key as a
450
- still-pending request, the previous one is aborted — only the last request
451
- executes:
462
+ The SDK auto-cancels duplicated pending requests for you (PocketBase-compatible behaviour). When a new request is issued with the same request key as a still-pending request, the previous one is aborted — only the last request executes:
452
463
 
453
464
  ```typescript
454
465
  // Only the last call will execute; the first two are auto-cancelled
@@ -457,9 +468,7 @@ await client.collection('posts').getList(2, 20); // cancelled
457
468
  await client.collection('posts').getList(3, 20); // executed
458
469
  ```
459
470
 
460
- By default the request key is `HTTP_METHOD + path` (e.g. `"GET /api/posts?page=1"`), so
461
- duplicate calls with identical URLs cancel each other. Cancelled requests reject
462
- with an `ApiError` whose `isAbort` is `true`:
471
+ By default the request key is `HTTP_METHOD + path` (e.g. `"GET /api/posts?page=1"`), so duplicate calls with identical URLs cancel each other. Cancelled requests reject with an `ApiError` whose `isAbort` is `true`:
463
472
 
464
473
  ```typescript
465
474
  try {
@@ -471,10 +480,9 @@ try {
471
480
  }
472
481
  ```
473
482
 
474
- #### Per-request control
483
+ ### Per-request control
475
484
 
476
- Pass `requestKey` in the request options to customize the key, or disable
477
- auto-cancellation for a specific request:
485
+ Pass `requestKey` in the request options to customize the key, or disable auto-cancellation for a specific request:
478
486
 
479
487
  ```typescript
480
488
  await client.collection('posts').getList(1, 20, { requestKey: 'my-list' }); // cancelled
@@ -484,7 +492,7 @@ await client.collection('posts').getList(1, 20, { requestKey: null }); // exec
484
492
  await client.collection('posts').getList(1, 20, { requestKey: null }); // executed
485
493
  ```
486
494
 
487
- #### Global control
495
+ ### Global control
488
496
 
489
497
  ```typescript
490
498
  // Disable auto-cancellation globally
@@ -495,13 +503,9 @@ client.cancelRequest('GET /api/posts?page=1');
495
503
  client.cancelAllRequests();
496
504
  ```
497
505
 
498
- #### Single-flight dedup (`getFullList`)
506
+ ### Single-flight dedup (`getFullList`)
499
507
 
500
- `getFullList()` (and `collections.getFullList()`) are **single-flight**: concurrent
501
- calls with the same effective options share one in-flight request instead of
502
- firing duplicates. This means the common pattern below results in **one**
503
- network request, and **both** callers resolve with the same data — no abort
504
- rejection:
508
+ `getFullList()` (and `collections.getFullList()`) are **single-flight**: concurrent calls with the same effective options share one in-flight request instead of firing duplicates. This means the common pattern below results in **one** network request, and **both** callers resolve with the same data — no abort rejection:
505
509
 
506
510
  ```typescript
507
511
  const [a, b] = await Promise.all([
@@ -511,13 +515,9 @@ const [a, b] = await Promise.all([
511
515
  // one GET fired; a === b
512
516
  ```
513
517
 
514
- Calls with **different** options (e.g. different `sort`/`filter`) are still
515
- distinct requests. Multi-page fetches continue to work normally — each page
516
- request is unique (page number is part of the URL), so pages never cancel each
517
- other.
518
+ Calls with **different** options (e.g. different `sort`/`filter`) are still distinct requests. Multi-page fetches continue to work normally — each page request is unique (page number is part of the URL), so pages never cancel each other.
518
519
 
519
- The underlying `singleFlight` option is also available on any request when you
520
- want to coalesce concurrent identical calls yourself:
520
+ The underlying `singleFlight` option is also available on any request when you want to coalesce concurrent identical calls yourself:
521
521
 
522
522
  ```typescript
523
523
  await client.collection('posts').getList(1, 20, { singleFlight: true });
@@ -620,11 +620,7 @@ client.collection('private_feed').subscribe('*', (e) => { ... });
620
620
 
621
621
  ### Anonymous / rule-based realtime
622
622
 
623
- Realtime subscriptions honor your API **and list rules** — matching PocketBase
624
- behavior. This means **non-logged-in users can subscribe** to collections whose
625
- list rules are public (empty `""` string) or anon-friendly
626
- (`@request.auth.*` filters). The SDK auto-connects the WebSocket on first use,
627
- so no token is required to receive public change events:
623
+ Realtime subscriptions honor your API **and list rules** — matching PocketBase behavior. This means **non-logged-in users can subscribe** to collections whose list rules are public (empty `""` string) or anon-friendly (`@request.auth.*` filters). The SDK auto-connects the WebSocket on first use, so no token is required to receive public change events:
628
624
 
629
625
  ```typescript
630
626
  // Works without logging in, as long as the collection's list rule allows it
@@ -668,12 +664,6 @@ There is no manual `workflow_dispatch` step and no local `npm publish`.
668
664
  - `feat(...)` commits → minor (`0.8.2` → `0.9.0`)
669
665
  - a `BREAKING CHANGE:` footer in any commit body → major (`0.8.2` → `1.0.0`)
670
666
 
671
- ### One-time setup
672
-
673
- - Add an npm access token (Automation or Publish scope, from
674
- <<https://www.npmjs.com/settings/><you>/tokens>) as the repo secret
675
- **`NPM_TOKEN`** under Settings → Secrets and variables → Actions.
676
-
677
667
  ## License
678
668
 
679
669
  [MIT](LICENSE) © 2024-2025 Chris Nguyen (gnuzd)
@@ -8,6 +8,7 @@ function fieldTypeScriptType(field, fallback = "unknown") {
8
8
  case "editor":
9
9
  case "date":
10
10
  case "datetime":
11
+ case "autodate":
11
12
  return "string";
12
13
  case "number":
13
14
  return "number";
@@ -51,6 +52,7 @@ function fieldTypeKind(field) {
51
52
  case "editor":
52
53
  case "date":
53
54
  case "datetime":
55
+ case "autodate":
54
56
  case "select":
55
57
  case "file":
56
58
  return "string";
@@ -96,6 +98,13 @@ function schemaFieldType(field) {
96
98
  }
97
99
 
98
100
  // src/codegen.ts
101
+ var BASE_RECORD_KEYS = /* @__PURE__ */ new Set([
102
+ "id",
103
+ "collectionId",
104
+ "collectionName",
105
+ "created",
106
+ "updated"
107
+ ]);
99
108
  function collectionTypeName(name) {
100
109
  return name.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("").replace(/^[0-9]/, "_$&");
101
110
  }
@@ -124,10 +133,13 @@ function generateTypes(collections, options = {}) {
124
133
  updated: string;
125
134
  }`);
126
135
  }
136
+ const collById = new Map(
137
+ filtered.filter((c) => c.id !== void 0 && c.id !== "").map((c) => [String(c.id), c])
138
+ );
127
139
  for (const coll of filtered) {
128
140
  const typeName = collectionTypeName(coll.name);
129
141
  const fields = coll.fields ?? [];
130
- const lines = fields.map((f) => memberLine(f)).filter((l) => l !== "");
142
+ const lines = fields.map((f) => memberLine(f, includeBaseFields)).filter((l) => l !== "");
131
143
  const body = lines.join("\n");
132
144
  sections.push(
133
145
  `export interface ${typeName}Record${renderInterface({
@@ -153,6 +165,7 @@ function generateTypes(collections, options = {}) {
153
165
  for (const f of fields) {
154
166
  if (f.type === "password") continue;
155
167
  const key = fieldKey(f.name);
168
+ if (includeBaseFields && BASE_RECORD_KEYS.has(key)) continue;
156
169
  const type = fieldTypeScriptType(f);
157
170
  if (type === "never") continue;
158
171
  queryLines.push(` ${JSON.stringify(key)}?: ${type};`);
@@ -163,6 +176,24 @@ function generateTypes(collections, options = {}) {
163
176
  body: queryLines.join("\n")
164
177
  })}`
165
178
  );
179
+ const expandLines = [];
180
+ for (const f of fields) {
181
+ if (f.type !== "relation") continue;
182
+ const target = collById.get(String(f.options?.collectionId ?? "")) ?? filtered.find(
183
+ (c) => c.name === String(f.options?.collection ?? "")
184
+ );
185
+ if (!target) continue;
186
+ const targetType = `${collectionTypeName(target.name)}Record${target.type === "auth" ? " & AuthRecord" : ""}`;
187
+ const many = (f.options?.maxSelect ?? 1) > 1;
188
+ expandLines.push(
189
+ ` ${JSON.stringify(fieldKey(f.name))}: ${many ? `Array<${targetType}>` : targetType};`
190
+ );
191
+ }
192
+ sections.push(
193
+ `export interface ${typeName}ExpandMap${renderInterface({
194
+ body: expandLines.join("\n")
195
+ })}`
196
+ );
166
197
  }
167
198
  sections.push(`export interface AuthRecord extends BaseRecord {
168
199
  email: string;
@@ -183,6 +214,10 @@ ${createMapEntries}
183
214
  ).join("\n");
184
215
  sections.push(`export interface LazypockQueryFields {
185
216
  ${queryMapEntries}
217
+ }`);
218
+ const expandMapEntries = filtered.map((c) => ` "${c.name}": ${collectionTypeName(c.name)}ExpandMap;`).join("\n");
219
+ sections.push(`export interface LazypockExpandMaps {
220
+ ${expandMapEntries}
186
221
  }`);
187
222
  const schemaEntries = collections.map(
188
223
  (c) => ` {
@@ -236,25 +271,31 @@ export class TypedClient extends LazypockClient {
236
271
  // The (string & {}) intersection keeps the union from collapsing to plain
237
272
  // string (which would silently kill the suggestions).
238
273
  //
239
- // The service binds three types: the read model, the write-only *CreateData,
240
- // and the *QueryFields (filter/sort/expand/select keys). QueryFields is what
241
- // lets hidden relation fields \u2014 excluded from the read model but still
242
- // expandable/filterable at runtime \u2014 typecheck:
274
+ // The service binds four types: the read model, the write-only *CreateData,
275
+ // the *QueryFields (filter/sort/expand/select keys), and the *ExpandMap
276
+ // (relation field \u2192 target record type). QueryFields is what lets hidden
277
+ // relation fields \u2014 excluded from the read model but still expandable/
278
+ // filterable at runtime \u2014 typecheck:
243
279
  // client.collection("project_members").getFullList({ expand: "user" }) // \u2713
280
+ // ExpandMap is what types the expanded data on the result:
281
+ // const [rec] = await client.collection("posts").getFullList({ expand: "user" });
282
+ // rec.expand?.user // \u2713 UsersRecord, not unknown
244
283
  override collection<K extends keyof LazypockCollections | (string & {})>(
245
284
  name: K,
246
285
  ): K extends keyof LazypockCollections
247
286
  ? CollectionService<
248
287
  LazypockCollections[K],
249
288
  LazypockCreateData[K],
250
- LazypockQueryFields[K]
289
+ LazypockQueryFields[K],
290
+ LazypockExpandMaps[K]
251
291
  >
252
292
  : CollectionService<unknown> {
253
293
  return super.collection(name) as K extends keyof LazypockCollections
254
294
  ? CollectionService<
255
295
  LazypockCollections[K],
256
296
  LazypockCreateData[K],
257
- LazypockQueryFields[K]
297
+ LazypockQueryFields[K],
298
+ LazypockExpandMaps[K]
258
299
  >
259
300
  : CollectionService<unknown>;
260
301
  }
@@ -269,9 +310,10 @@ function renderInterface(opts) {
269
310
  ${opts.body}
270
311
  }`;
271
312
  }
272
- function memberLine(f) {
313
+ function memberLine(f, skipBaseKeys = false) {
273
314
  if (f.hidden) return "";
274
315
  const key = fieldKey(f.name);
316
+ if (skipBaseKeys && BASE_RECORD_KEYS.has(key)) return "";
275
317
  const req = f.required || f.type === "password" ? "" : "?";
276
318
  const type = fieldTypeScriptType(f);
277
319
  if (type === "never") return "";
@@ -303,4 +345,4 @@ export {
303
345
  collectionTypeName,
304
346
  generateTypes
305
347
  };
306
- //# sourceMappingURL=chunk-LVEOMG6A.js.map
348
+ //# sourceMappingURL=chunk-HA22OVUR.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/typegen.ts","../src/codegen.ts"],"sourcesContent":["// ── Type mapping (runtime + codegen) ────────────────────\n// Maps server field types to TypeScript types.\n// Shared by the runtime `SchemaTypes` helper and the codegen CLI.\n\nimport type { SchemaField } from \"./schema\";\n\n/**\n * Map a single server field to its TypeScript type string.\n * Used by the codegen CLI to emit interface members.\n *\n * @param field The field definition.\n * @param fallback Fallback type for unknown field types (default `unknown`).\n */\nexport function fieldTypeScriptType(\n\tfield: SchemaField,\n\tfallback = \"unknown\",\n): string {\n\tconst opts = field.options ?? {};\n\tswitch (field.type) {\n\t\tcase \"text\":\n\t\tcase \"email\":\n\t\tcase \"url\":\n\t\tcase \"editor\":\n\t\tcase \"date\":\n\t\tcase \"datetime\":\n\t\tcase \"autodate\":\n\t\t\treturn \"string\";\n\t\tcase \"number\":\n\t\t\treturn \"number\";\n\t\tcase \"bool\":\n\t\t\treturn \"boolean\";\n\t\tcase \"select\": {\n\t\t\tconst values = Array.isArray(opts.values) ? opts.values : [];\n\t\t\tif (values.length > 0) {\n\t\t\t\treturn values.map((v) => JSON.stringify(String(v))).join(\" | \");\n\t\t\t}\n\t\t\treturn \"string\";\n\t\t}\n\t\tcase \"multi_select\": {\n\t\t\tconst values = Array.isArray(opts.values) ? opts.values : [];\n\t\t\tif (values.length > 0) {\n\t\t\t\treturn `(${values.map((v) => JSON.stringify(String(v))).join(\" | \")})[]`;\n\t\t\t}\n\t\t\treturn \"string[]\";\n\t\t}\n\t\tcase \"file\":\n\t\t\treturn \"string\";\n\t\tcase \"multi_file\":\n\t\t\treturn \"string[]\";\n\t\tcase \"json\":\n\t\tcase \"geo\":\n\t\t\treturn \"Record<string, unknown>\";\n\t\tcase \"relation\":\n\t\t\t// Relations store the target record's ID (string) — or an array\n\t\t\t// of IDs when multi-relation (maxSelect > 1).\n\t\t\treturn (opts.maxSelect ?? 1) > 1 ? \"string[]\" : \"string\";\n\t\tcase \"password\":\n\t\t\t// Passwords are write-only; never expose on read models.\n\t\t\treturn \"never\";\n\t\tdefault:\n\t\t\treturn fallback;\n\t}\n}\n\n/**\n * Returns the runtime type kind for a field — used by {@link schemaFieldType}\n * to build structural types at runtime.\n */\nexport type FieldTypeKind =\n\t| \"string\"\n\t| \"number\"\n\t| \"boolean\"\n\t| \"string-array\"\n\t| \"json\"\n\t| \"relation\"\n\t| \"relation-many\"\n\t| \"password\"\n\t| \"unknown\";\n\n/** Map a server field to its runtime type kind. */\nexport function fieldTypeKind(field: SchemaField): FieldTypeKind {\n\tconst opts = field.options ?? {};\n\tswitch (field.type) {\n\t\tcase \"text\":\n\t\tcase \"email\":\n\t\tcase \"url\":\n\t\tcase \"editor\":\n\t\tcase \"date\":\n\t\tcase \"datetime\":\n\t\tcase \"autodate\":\n\t\tcase \"select\":\n\t\tcase \"file\":\n\t\t\treturn \"string\";\n\t\tcase \"number\":\n\t\t\treturn \"number\";\n\t\tcase \"bool\":\n\t\t\treturn \"boolean\";\n\t\tcase \"multi_select\":\n\t\tcase \"multi_file\":\n\t\t\treturn \"string-array\";\n\t\tcase \"json\":\n\t\tcase \"geo\":\n\t\t\treturn \"json\";\n\t\tcase \"relation\":\n\t\t\treturn (opts.maxSelect ?? 1) > 1 ? \"relation-many\" : \"relation\";\n\t\tcase \"password\":\n\t\t\treturn \"password\";\n\t\tdefault:\n\t\t\treturn \"unknown\";\n\t}\n}\n\n/**\n * Derive a TypeScript field type from a {@link SchemaField} — the runtime\n * counterpart to the codegen mapper. Lets consumers build typed clients\n * from a fetched schema without running the CLI.\n */\nexport function schemaFieldType(field: SchemaField): unknown {\n\tswitch (fieldTypeKind(field)) {\n\t\tcase \"string\":\n\t\t\treturn String;\n\t\tcase \"number\":\n\t\t\treturn Number;\n\t\tcase \"boolean\":\n\t\t\treturn Boolean;\n\t\tcase \"string-array\":\n\t\t\treturn [String] as const;\n\t\tcase \"relation\":\n\t\t\treturn String;\n\t\tcase \"relation-many\":\n\t\t\treturn [String] as const;\n\t\tcase \"json\":\n\t\t\treturn Object;\n\t\tcase \"password\":\n\t\t\treturn undefined;\n\t\tcase \"unknown\":\n\t\t\treturn undefined;\n\t}\n}\n","// ── Codegen ─────────────────────────────────────────────\n// Generates a `lazypock.types.ts` module from the live API schema.\n//\n// The generated file exports:\n// - One interface per collection (e.g. `PostsRecord`)\n// - A `LazypockCollections` map: { posts: PostsRecord; users: UsersRecord; ... }\n// - A `createClient()` factory pre-bound to those types, so\n// `client.collection(\"posts\").create({ title: \"x\" })` is fully type-checked.\n//\n// The CLI in `src/cli.ts` wires this to `GET /collections`.\n\nimport type { CollectionSchema, SchemaField } from \"./schema\";\nimport { fieldTypeScriptType } from \"./typegen\";\n\n/**\n * Members already declared by the generated `BaseRecord` interface. A\n * collection field with one of these names must NOT be re-declared on the\n * per-collection read/query interfaces: redeclaring `created`/`updated`\n * (PocketBase-style autodate fields, which the mapper types as `string`) as\n * an optional member makes TypeScript reject the interface with TS2430\n * (\"incorrectly extends interface 'BaseRecord'\") because the base declares\n * them as required `string`. The base declaration is kept.\n */\nconst BASE_RECORD_KEYS = new Set([\n\t\"id\",\n\t\"collectionId\",\n\t\"collectionName\",\n\t\"created\",\n\t\"updated\",\n]);\n\n/** Format a raw collection name into a valid TS identifier (PascalCase). */\nexport function collectionTypeName(name: string): string {\n\treturn name\n\t\t.split(/[^a-zA-Z0-9]+/)\n\t\t.filter(Boolean)\n\t\t.map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n\t\t.join(\"\")\n\t\t.replace(/^[0-9]/, \"_$&\");\n}\n\n/** Interface member name — sanitize hyphens/spaces but keep readability. */\nexport function fieldKey(name: string): string {\n\treturn name.replace(/[^a-zA-Z0-9_]/g, \"_\");\n}\n\n\n\n/**\n * Generate the full TypeScript source for the typed SDK module.\n *\n * @param collections Collections fetched from the API.\n * @param options Generation options.\n */\nexport function generateTypes(\n\tcollections: CollectionSchema[],\n\toptions: {\n\t\t/** Import specifier for the lazypock package (default `lazypock`). */\n\t\tpackageName?: string;\n\t\t/** Emit base record fields (id, created, updated, …). Default true. */\n\t\tincludeBaseFields?: boolean;\n\t\t/** Skip system collections (names starting with `_` or `users`). Default false. */\n\t\tskipSystem?: boolean;\n\t} = {},\n): string {\n\tconst {\n\t\tpackageName = \"lazypock\",\n\t\tincludeBaseFields = true,\n\t\tskipSystem = false,\n\t} = options;\n\n\tconst filtered = skipSystem\n\t\t? collections.filter(\n\t\t\t\t(c) => !c.system && !c.name.startsWith(\"_\") && c.name !== \"users\",\n\t\t\t)\n\t\t: collections;\n\n\tconst sections: string[] = [];\n\tsections.push(`// ── Auto-generated by lazypock-ts ──────────────────────────\n// Do not edit by hand. Regenerate with: npx lazypock-gen\n// Schema snapshot: ${new Date().toISOString()}`);\n\n\tif (includeBaseFields) {\n\t\tsections.push(`export interface BaseRecord {\n id: string;\n collectionId: string;\n collectionName: string;\n created: string;\n updated: string;\n}`);\n\t}\n\n\t// Resolve relation targets (collectionId → record type) for the expand maps.\n\t// Only collections with a real id are indexed — schemas without ids (e.g.\n\t// minimal hand-written snapshots) fall back to the name lookup below.\n\tconst collById = new Map(\n\t\tfiltered\n\t\t\t.filter((c) => c.id !== undefined && c.id !== \"\")\n\t\t\t.map((c) => [String(c.id), c]),\n\t);\n\n\t// One interface per collection\n\tfor (const coll of filtered) {\n\t\tconst typeName = collectionTypeName(coll.name);\n\t\tconst fields = coll.fields ?? [];\n\t\tconst lines = fields\n\t\t\t.map((f) => memberLine(f, includeBaseFields))\n\t\t\t.filter((l) => l !== \"\");\n\t\tconst body = lines.join(\"\\n\");\n\t\tsections.push(\n\t\t\t`export interface ${typeName}Record${renderInterface({\n\t\t\t\textends: includeBaseFields ? \"BaseRecord\" : undefined,\n\t\t\t\tbody,\n\t\t\t})}`,\n\t\t);\n\n\t\t// Write-only create data: what `create()`/`update()` accept. This\n\t\t// intentionally includes password fields (and hidden fields) that are\n\t\t// excluded from the read model — the server accepts them on write,\n\t\t// hashes passwords, and never returns them in responses.\n\t\tconst createLines: string[] = [];\n\t\tconst seenKeys = new Set<string>();\n\t\tfor (const f of fields) {\n\t\t\tfor (const { key, line } of createDataMemberLines(f)) {\n\t\t\t\tif (seenKeys.has(key)) continue;\n\t\t\t\tseenKeys.add(key);\n\t\t\t\tcreateLines.push(line);\n\t\t\t}\n\t\t}\n\t\tsections.push(\n\t\t\t`export interface ${typeName}CreateData${renderInterface({\n\t\t\t\tbody: createLines.join(\"\\n\"),\n\t\t\t})}`,\n\t\t);\n\n\t\t// Query fields — the key set for filter/sort/expand/select. This\n\t\t// intentionally includes hidden fields: a hidden relation is still\n\t\t// expandable/filterable at runtime (the server resolves it) even\n\t\t// though it is excluded from the read model. Only password fields\n\t\t// are never queryable.\n\t\tconst queryLines: string[] = [];\n\t\tfor (const f of fields) {\n\t\t\tif (f.type === \"password\") continue;\n\t\t\tconst key = fieldKey(f.name);\n\t\t\t// The query interface extends BaseRecord too (when base fields are\n\t\t\t// emitted), so the same key collisions apply.\n\t\t\tif (includeBaseFields && BASE_RECORD_KEYS.has(key)) continue;\n\t\t\tconst type = fieldTypeScriptType(f);\n\t\t\tif (type === \"never\") continue;\n\t\t\tqueryLines.push(` ${JSON.stringify(key)}?: ${type};`);\n\t\t}\n\t\tsections.push(\n\t\t\t`export interface ${typeName}QueryFields${renderInterface({\n\t\t\t\textends: includeBaseFields ? \"BaseRecord\" : undefined,\n\t\t\t\tbody: queryLines.join(\"\\n\"),\n\t\t\t})}`,\n\t\t);\n\n\t\t// Expand map — relation field → target record type, so expanded\n\t\t// records are typed (`record.expand.user` is `UsersRecord` instead of\n\t\t// `unknown`). Built from each relation's `options.collectionId`; auth\n\t\t// targets get `& AuthRecord`, multi-relations (maxSelect > 1) get\n\t\t// arrays, and unknown targets (e.g. skipped collections) fall back to\n\t\t// `unknown` via the `ExpandValue` fallback.\n\t\tconst expandLines: string[] = [];\n\t\tfor (const f of fields) {\n\t\t\tif (f.type !== \"relation\") continue;\n\t\t\t// Resolve the target collection: real PocketBase schemas carry\n\t\t\t// `options.collectionId` (the target collection's id), while some\n\t\t\t// tooling/snapshots use `options.collection` (the target's name).\n\t\t\t// Unknown/absent targets fall back to `unknown` via ExpandValue.\n\t\t\t// SAFETY: both option keys are strings; guard with String().\n\t\t\tconst target =\n\t\t\t\tcollById.get(String(f.options?.collectionId ?? \"\")) ??\n\t\t\t\tfiltered.find(\n\t\t\t\t\t(c) => c.name === String(f.options?.collection ?? \"\"),\n\t\t\t\t);\n\t\t\tif (!target) continue;\n\t\t\tconst targetType = `${collectionTypeName(target.name)}Record${\n\t\t\t\ttarget.type === \"auth\" ? \" & AuthRecord\" : \"\"\n\t\t\t}`;\n\t\t\tconst many = (f.options?.maxSelect ?? 1) > 1;\n\t\t\texpandLines.push(\n\t\t\t\t` ${JSON.stringify(fieldKey(f.name))}: ${\n\t\t\t\t\tmany ? `Array<${targetType}>` : targetType\n\t\t\t\t};`,\n\t\t\t);\n\t\t}\n\t\tsections.push(\n\t\t\t`export interface ${typeName}ExpandMap${renderInterface({\n\t\t\t\tbody: expandLines.join(\"\\n\"),\n\t\t\t})}`,\n\t\t);\n\t}\n\n\t// Auth collection type\n\tsections.push(`export interface AuthRecord extends BaseRecord {\n email: string;\n verified: boolean;\n}`);\n\n\t// Collections map\n\tconst mapEntries = filtered\n\t\t.map(\n\t\t\t(c) =>\n\t\t\t\t` \"${c.name}\": ${collectionTypeName(c.name)}Record${\n\t\t\t\t\tc.type === \"auth\" ? \" & AuthRecord\" : \"\"\n\t\t\t\t};`,\n\t\t)\n\t\t.join(\"\\n\");\n\tsections.push(`export interface LazypockCollections {\n${mapEntries}\n}`);\n\n\t// Create-data map — typed `create()`/`update()` payloads, including\n\t// write-only password fields.\n\tconst createMapEntries = filtered\n\t\t.map((c) => ` \"${c.name}\": ${collectionTypeName(c.name)}CreateData;`)\n\t\t.join(\"\\n\");\n\tsections.push(`export interface LazypockCreateData {\n${createMapEntries}\n}`);\n\n\t// Query-fields map — filter/sort/expand/select keys per collection,\n\t// including hidden fields (valid at runtime despite being excluded from\n\t// the read model). Auth collections get the auth system keys too.\n\tconst queryMapEntries = filtered\n\t\t.map(\n\t\t\t(c) =>\n\t\t\t\t` \"${c.name}\": ${collectionTypeName(c.name)}QueryFields${\n\t\t\t\t\tc.type === \"auth\" ? \" & AuthRecord\" : \"\"\n\t\t\t\t};`,\n\t\t)\n\t\t.join(\"\\n\");\n\tsections.push(`export interface LazypockQueryFields {\n${queryMapEntries}\n}`);\n\n\t// Expand map — relation field → target record type per collection, bound\n\t// as the 4th CollectionService generic so expanded records are typed.\n\tconst expandMapEntries = filtered\n\t\t.map((c) => ` \"${c.name}\": ${collectionTypeName(c.name)}ExpandMap;`)\n\t\t.join(\"\\n\");\n\tsections.push(`export interface LazypockExpandMaps {\n${expandMapEntries}\n}`);\n\n\t// Runtime schema snapshot — lets the client exclude hidden fields by\n\t// default, validate select/expand, and derive visible field lists.\n\tconst schemaEntries = collections\n\t\t.map(\n\t\t\t(c) =>\n\t\t\t\t` {\n id: ${JSON.stringify(c.id ?? null)},\n name: ${JSON.stringify(c.name)},\n type: ${JSON.stringify(c.type)},\n system: ${JSON.stringify(c.system ?? false)},\n fields: ${JSON.stringify(c.fields ?? [], null, 2)},\n rules: ${JSON.stringify(c.rules ?? {})},\n options: ${JSON.stringify(c.options ?? {})},\n }`,\n\t\t)\n\t\t.join(\",\\n\");\n\tsections.push(`// Schema snapshot (runtime) — hidden-field exclusion + query validation.\nexport const lazypockSchema: import(\"${packageName}\").CollectionSchema[] = [\n${schemaEntries}\n];`);\n\n\t// createClient factory\n\tsections.push(`import { LazypockClient, type LazypockClientOptions, type CollectionService } from \"${packageName}\";\n\n/**\n * Create a Lazypock client typed against this schema snapshot.\n * Collection access is fully type-checked:\n * client.collection(\"posts\").create({ title: \"x\" }) // title must exist\n *\n * For auth collections the generated *CreateData type includes the\n * write-only password field, so creating a user is type-safe:\n * client.collection(\"users\").create({ email, password }) // ✓\n *\n * The schema snapshot is wired into the client automatically, so hidden\n * fields are excluded from responses and select/expand are validated.\n */\nexport function createClient(options: LazypockClientOptions): TypedClient {\n return new TypedClient({\n ...options,\n types: {\n ...options.types,\n schemas: options.types?.schemas ?? lazypockSchema,\n },\n });\n}\n\nexport class TypedClient extends LazypockClient {\n // The constraint is \"keyof LazypockCollections | (string & {})\" rather than a\n // bare generic string or a strict keyof:\n // - the keyof LazypockCollections member is what makes the IDE suggest\n // collection names, and known literals resolve to the typed service:\n // client.collection(\"posts\") // suggested + typed\n // - the string & {} member keeps unknown/dynamic names compiling — they\n // resolve to the untyped service, so a studio that manages\n // user-created collections can still call collection(someString).\n //\n // The (string & {}) intersection keeps the union from collapsing to plain\n // string (which would silently kill the suggestions).\n //\n // The service binds four types: the read model, the write-only *CreateData,\n // the *QueryFields (filter/sort/expand/select keys), and the *ExpandMap\n // (relation field → target record type). QueryFields is what lets hidden\n // relation fields — excluded from the read model but still expandable/\n // filterable at runtime — typecheck:\n // client.collection(\"project_members\").getFullList({ expand: \"user\" }) // ✓\n // ExpandMap is what types the expanded data on the result:\n // const [rec] = await client.collection(\"posts\").getFullList({ expand: \"user\" });\n // rec.expand?.user // ✓ UsersRecord, not unknown\n override collection<K extends keyof LazypockCollections | (string & {})>(\n name: K,\n ): K extends keyof LazypockCollections\n ? CollectionService<\n LazypockCollections[K],\n LazypockCreateData[K],\n LazypockQueryFields[K],\n LazypockExpandMaps[K]\n >\n : CollectionService<unknown> {\n return super.collection(name) as K extends keyof LazypockCollections\n ? CollectionService<\n LazypockCollections[K],\n LazypockCreateData[K],\n LazypockQueryFields[K],\n LazypockExpandMaps[K]\n >\n : CollectionService<unknown>;\n }\n}\n`);\n\n\treturn sections.join(\"\\n\\n\") + \"\\n\";\n}\n\n/**\n * Render the interface body including the extends clause.\n * Empty body → ` extends BaseRecord {}` (valid TS).\n */\nfunction renderInterface(opts: { extends?: string; body: string }): string {\n\tconst ext = opts.extends ? ` extends ${opts.extends}` : \"\";\n\tif (!opts.body) return `${ext} {}`;\n\treturn `${ext} {\\n${opts.body}\\n}`;\n}\n\n/**\n * Render a single interface member line for a field.\n *\n * `skipBaseKeys` is true when the interface extends `BaseRecord`: keys the\n * base already declares are omitted so the child never redeclares them with\n * an incompatible (optional / non-string) type.\n */\nfunction memberLine(f: SchemaField, skipBaseKeys = false): string {\n\t// Hidden fields are excluded from API responses by the schema-aware\n\t// client default — don't expose them on the read model either.\n\tif (f.hidden) return \"\";\n\tconst key = fieldKey(f.name);\n\tif (skipBaseKeys && BASE_RECORD_KEYS.has(key)) return \"\";\n\tconst req = f.required || f.type === \"password\" ? \"\" : \"?\";\n\tconst type = fieldTypeScriptType(f);\n\t// `never` members (passwords) are omitted from read models.\n\tif (type === \"never\") return \"\";\n\treturn ` ${JSON.stringify(key)}${req}: ${type};`;\n}\n\n/**\n * Render the write-only create-data members for a field.\n *\n * Unlike the read model, create/update data includes password fields (sent as\n * plain strings — the server hashes them) and hidden fields: the server\n * accepts them on write and never returns them in responses.\n *\n * Password fields are exposed under the canonical PocketBase API key\n * `password` (the server accepts `password` for any single password field and\n * hashes it into the backing column, e.g. `password_hash`), with the raw\n * metadata name kept as a backward-compat alias.\n *\n * A field is only marked required when it actually needs a client value:\n * fields with a server-side default (`options.defaultValue`) and the auth\n * system fields `verified` / `emailVisibility` (the server defaults them to\n * false/true even though the snapshot doesn't carry `defaultValue`) stay\n * optional, so `create({ email, password })` typechecks without forcing\n * callers to send values the server would fill in anyway. Password fields\n * are optional (accounts may exist without a password, e.g. OAuth-only).\n */\nfunction createDataMemberLines(\n\tf: SchemaField,\n): { key: string; line: string }[] {\n\t// Autodate columns are server-managed; never writable.\n\tif (f.type === \"autodate\") return [];\n\tconst rawName = fieldKey(f.name);\n\tconst serverDefaulted =\n\t\tf.options?.defaultValue !== undefined ||\n\t\t(f.system && (f.name === \"verified\" || f.name === \"emailVisibility\"));\n\tconst req = f.required && !serverDefaulted ? \"\" : \"?\";\n\tconst type = f.type === \"password\" ? \"string\" : fieldTypeScriptType(f);\n\tif (type === \"never\") return [];\n\n\tconst members = [\n\t\t{ key: rawName, line: ` ${JSON.stringify(rawName)}${req}: ${type};` },\n\t];\n\t// Write-only password: canonical key is `password` (PocketBase parity) —\n\t// the server accepts `password` for any single password field and hashes\n\t// it. The raw metadata name (e.g. `password_hash`) stays as a compat alias.\n\tif (f.type === \"password\" && rawName !== \"password\") {\n\t\tmembers.unshift({\n\t\t\tkey: \"password\",\n\t\t\tline: ` ${JSON.stringify(\"password\")}${req}: ${type};`,\n\t\t});\n\t}\n\treturn members;\n}\n"],"mappings":";AAaO,SAAS,oBACf,OACA,WAAW,WACF;AACT,QAAM,OAAO,MAAM,WAAW,CAAC;AAC/B,UAAQ,MAAM,MAAM;AAAA,IACnB,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AACJ,aAAO;AAAA,IACR,KAAK;AACJ,aAAO;AAAA,IACR,KAAK;AACJ,aAAO;AAAA,IACR,KAAK,UAAU;AACd,YAAM,SAAS,MAAM,QAAQ,KAAK,MAAM,IAAI,KAAK,SAAS,CAAC;AAC3D,UAAI,OAAO,SAAS,GAAG;AACtB,eAAO,OAAO,IAAI,CAAC,MAAM,KAAK,UAAU,OAAO,CAAC,CAAC,CAAC,EAAE,KAAK,KAAK;AAAA,MAC/D;AACA,aAAO;AAAA,IACR;AAAA,IACA,KAAK,gBAAgB;AACpB,YAAM,SAAS,MAAM,QAAQ,KAAK,MAAM,IAAI,KAAK,SAAS,CAAC;AAC3D,UAAI,OAAO,SAAS,GAAG;AACtB,eAAO,IAAI,OAAO,IAAI,CAAC,MAAM,KAAK,UAAU,OAAO,CAAC,CAAC,CAAC,EAAE,KAAK,KAAK,CAAC;AAAA,MACpE;AACA,aAAO;AAAA,IACR;AAAA,IACA,KAAK;AACJ,aAAO;AAAA,IACR,KAAK;AACJ,aAAO;AAAA,IACR,KAAK;AAAA,IACL,KAAK;AACJ,aAAO;AAAA,IACR,KAAK;AAGJ,cAAQ,KAAK,aAAa,KAAK,IAAI,aAAa;AAAA,IACjD,KAAK;AAEJ,aAAO;AAAA,IACR;AACC,aAAO;AAAA,EACT;AACD;AAkBO,SAAS,cAAc,OAAmC;AAChE,QAAM,OAAO,MAAM,WAAW,CAAC;AAC/B,UAAQ,MAAM,MAAM;AAAA,IACnB,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AACJ,aAAO;AAAA,IACR,KAAK;AACJ,aAAO;AAAA,IACR,KAAK;AACJ,aAAO;AAAA,IACR,KAAK;AAAA,IACL,KAAK;AACJ,aAAO;AAAA,IACR,KAAK;AAAA,IACL,KAAK;AACJ,aAAO;AAAA,IACR,KAAK;AACJ,cAAQ,KAAK,aAAa,KAAK,IAAI,kBAAkB;AAAA,IACtD,KAAK;AACJ,aAAO;AAAA,IACR;AACC,aAAO;AAAA,EACT;AACD;AAOO,SAAS,gBAAgB,OAA6B;AAC5D,UAAQ,cAAc,KAAK,GAAG;AAAA,IAC7B,KAAK;AACJ,aAAO;AAAA,IACR,KAAK;AACJ,aAAO;AAAA,IACR,KAAK;AACJ,aAAO;AAAA,IACR,KAAK;AACJ,aAAO,CAAC,MAAM;AAAA,IACf,KAAK;AACJ,aAAO;AAAA,IACR,KAAK;AACJ,aAAO,CAAC,MAAM;AAAA,IACf,KAAK;AACJ,aAAO;AAAA,IACR,KAAK;AACJ,aAAO;AAAA,IACR,KAAK;AACJ,aAAO;AAAA,EACT;AACD;;;ACnHA,IAAM,mBAAmB,oBAAI,IAAI;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACD,CAAC;AAGM,SAAS,mBAAmB,MAAsB;AACxD,SAAO,KACL,MAAM,eAAe,EACrB,OAAO,OAAO,EACd,IAAI,CAAC,SAAS,KAAK,OAAO,CAAC,EAAE,YAAY,IAAI,KAAK,MAAM,CAAC,CAAC,EAC1D,KAAK,EAAE,EACP,QAAQ,UAAU,KAAK;AAC1B;AAGO,SAAS,SAAS,MAAsB;AAC9C,SAAO,KAAK,QAAQ,kBAAkB,GAAG;AAC1C;AAUO,SAAS,cACf,aACA,UAOI,CAAC,GACI;AACT,QAAM;AAAA,IACL,cAAc;AAAA,IACd,oBAAoB;AAAA,IACpB,aAAa;AAAA,EACd,IAAI;AAEJ,QAAM,WAAW,aACd,YAAY;AAAA,IACZ,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC,EAAE,KAAK,WAAW,GAAG,KAAK,EAAE,SAAS;AAAA,EAC3D,IACC;AAEH,QAAM,WAAqB,CAAC;AAC5B,WAAS,KAAK;AAAA;AAAA,uBAEO,oBAAI,KAAK,GAAE,YAAY,CAAC,EAAE;AAE/C,MAAI,mBAAmB;AACtB,aAAS,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMd;AAAA,EACD;AAKA,QAAM,WAAW,IAAI;AAAA,IACpB,SACE,OAAO,CAAC,MAAM,EAAE,OAAO,UAAa,EAAE,OAAO,EAAE,EAC/C,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,GAAG,CAAC,CAAC;AAAA,EAC/B;AAGA,aAAW,QAAQ,UAAU;AAC5B,UAAM,WAAW,mBAAmB,KAAK,IAAI;AAC7C,UAAM,SAAS,KAAK,UAAU,CAAC;AAC/B,UAAM,QAAQ,OACZ,IAAI,CAAC,MAAM,WAAW,GAAG,iBAAiB,CAAC,EAC3C,OAAO,CAAC,MAAM,MAAM,EAAE;AACxB,UAAM,OAAO,MAAM,KAAK,IAAI;AAC5B,aAAS;AAAA,MACR,oBAAoB,QAAQ,SAAS,gBAAgB;AAAA,QACpD,SAAS,oBAAoB,eAAe;AAAA,QAC5C;AAAA,MACD,CAAC,CAAC;AAAA,IACH;AAMA,UAAM,cAAwB,CAAC;AAC/B,UAAM,WAAW,oBAAI,IAAY;AACjC,eAAW,KAAK,QAAQ;AACvB,iBAAW,EAAE,KAAK,KAAK,KAAK,sBAAsB,CAAC,GAAG;AACrD,YAAI,SAAS,IAAI,GAAG,EAAG;AACvB,iBAAS,IAAI,GAAG;AAChB,oBAAY,KAAK,IAAI;AAAA,MACtB;AAAA,IACD;AACA,aAAS;AAAA,MACR,oBAAoB,QAAQ,aAAa,gBAAgB;AAAA,QACxD,MAAM,YAAY,KAAK,IAAI;AAAA,MAC5B,CAAC,CAAC;AAAA,IACH;AAOA,UAAM,aAAuB,CAAC;AAC9B,eAAW,KAAK,QAAQ;AACvB,UAAI,EAAE,SAAS,WAAY;AAC3B,YAAM,MAAM,SAAS,EAAE,IAAI;AAG3B,UAAI,qBAAqB,iBAAiB,IAAI,GAAG,EAAG;AACpD,YAAM,OAAO,oBAAoB,CAAC;AAClC,UAAI,SAAS,QAAS;AACtB,iBAAW,KAAK,KAAK,KAAK,UAAU,GAAG,CAAC,MAAM,IAAI,GAAG;AAAA,IACtD;AACA,aAAS;AAAA,MACR,oBAAoB,QAAQ,cAAc,gBAAgB;AAAA,QACzD,SAAS,oBAAoB,eAAe;AAAA,QAC5C,MAAM,WAAW,KAAK,IAAI;AAAA,MAC3B,CAAC,CAAC;AAAA,IACH;AAQA,UAAM,cAAwB,CAAC;AAC/B,eAAW,KAAK,QAAQ;AACvB,UAAI,EAAE,SAAS,WAAY;AAM3B,YAAM,SACL,SAAS,IAAI,OAAO,EAAE,SAAS,gBAAgB,EAAE,CAAC,KAClD,SAAS;AAAA,QACR,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE,SAAS,cAAc,EAAE;AAAA,MACrD;AACD,UAAI,CAAC,OAAQ;AACb,YAAM,aAAa,GAAG,mBAAmB,OAAO,IAAI,CAAC,SACpD,OAAO,SAAS,SAAS,kBAAkB,EAC5C;AACA,YAAM,QAAQ,EAAE,SAAS,aAAa,KAAK;AAC3C,kBAAY;AAAA,QACX,KAAK,KAAK,UAAU,SAAS,EAAE,IAAI,CAAC,CAAC,KACpC,OAAO,SAAS,UAAU,MAAM,UACjC;AAAA,MACD;AAAA,IACD;AACA,aAAS;AAAA,MACR,oBAAoB,QAAQ,YAAY,gBAAgB;AAAA,QACvD,MAAM,YAAY,KAAK,IAAI;AAAA,MAC5B,CAAC,CAAC;AAAA,IACH;AAAA,EACD;AAGA,WAAS,KAAK;AAAA;AAAA;AAAA,EAGb;AAGD,QAAM,aAAa,SACjB;AAAA,IACA,CAAC,MACA,MAAM,EAAE,IAAI,MAAM,mBAAmB,EAAE,IAAI,CAAC,SAC3C,EAAE,SAAS,SAAS,kBAAkB,EACvC;AAAA,EACF,EACC,KAAK,IAAI;AACX,WAAS,KAAK;AAAA,EACb,UAAU;AAAA,EACV;AAID,QAAM,mBAAmB,SACvB,IAAI,CAAC,MAAM,MAAM,EAAE,IAAI,MAAM,mBAAmB,EAAE,IAAI,CAAC,aAAa,EACpE,KAAK,IAAI;AACX,WAAS,KAAK;AAAA,EACb,gBAAgB;AAAA,EAChB;AAKD,QAAM,kBAAkB,SACtB;AAAA,IACA,CAAC,MACA,MAAM,EAAE,IAAI,MAAM,mBAAmB,EAAE,IAAI,CAAC,cAC3C,EAAE,SAAS,SAAS,kBAAkB,EACvC;AAAA,EACF,EACC,KAAK,IAAI;AACX,WAAS,KAAK;AAAA,EACb,eAAe;AAAA,EACf;AAID,QAAM,mBAAmB,SACvB,IAAI,CAAC,MAAM,MAAM,EAAE,IAAI,MAAM,mBAAmB,EAAE,IAAI,CAAC,YAAY,EACnE,KAAK,IAAI;AACX,WAAS,KAAK;AAAA,EACb,gBAAgB;AAAA,EAChB;AAID,QAAM,gBAAgB,YACpB;AAAA,IACA,CAAC,MACA;AAAA,UACM,KAAK,UAAU,EAAE,MAAM,IAAI,CAAC;AAAA,YAC1B,KAAK,UAAU,EAAE,IAAI,CAAC;AAAA,YACtB,KAAK,UAAU,EAAE,IAAI,CAAC;AAAA,cACpB,KAAK,UAAU,EAAE,UAAU,KAAK,CAAC;AAAA,cACjC,KAAK,UAAU,EAAE,UAAU,CAAC,GAAG,MAAM,CAAC,CAAC;AAAA,aACxC,KAAK,UAAU,EAAE,SAAS,CAAC,CAAC,CAAC;AAAA,eAC3B,KAAK,UAAU,EAAE,WAAW,CAAC,CAAC,CAAC;AAAA;AAAA,EAE5C,EACC,KAAK,KAAK;AACZ,WAAS,KAAK;AAAA,uCACwB,WAAW;AAAA,EAChD,aAAa;AAAA,GACZ;AAGF,WAAS,KAAK,uFAAuF,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAkEhH;AAEA,SAAO,SAAS,KAAK,MAAM,IAAI;AAChC;AAMA,SAAS,gBAAgB,MAAkD;AAC1E,QAAM,MAAM,KAAK,UAAU,YAAY,KAAK,OAAO,KAAK;AACxD,MAAI,CAAC,KAAK,KAAM,QAAO,GAAG,GAAG;AAC7B,SAAO,GAAG,GAAG;AAAA,EAAO,KAAK,IAAI;AAAA;AAC9B;AASA,SAAS,WAAW,GAAgB,eAAe,OAAe;AAGjE,MAAI,EAAE,OAAQ,QAAO;AACrB,QAAM,MAAM,SAAS,EAAE,IAAI;AAC3B,MAAI,gBAAgB,iBAAiB,IAAI,GAAG,EAAG,QAAO;AACtD,QAAM,MAAM,EAAE,YAAY,EAAE,SAAS,aAAa,KAAK;AACvD,QAAM,OAAO,oBAAoB,CAAC;AAElC,MAAI,SAAS,QAAS,QAAO;AAC7B,SAAO,KAAK,KAAK,UAAU,GAAG,CAAC,GAAG,GAAG,KAAK,IAAI;AAC/C;AAsBA,SAAS,sBACR,GACkC;AAElC,MAAI,EAAE,SAAS,WAAY,QAAO,CAAC;AACnC,QAAM,UAAU,SAAS,EAAE,IAAI;AAC/B,QAAM,kBACL,EAAE,SAAS,iBAAiB,UAC3B,EAAE,WAAW,EAAE,SAAS,cAAc,EAAE,SAAS;AACnD,QAAM,MAAM,EAAE,YAAY,CAAC,kBAAkB,KAAK;AAClD,QAAM,OAAO,EAAE,SAAS,aAAa,WAAW,oBAAoB,CAAC;AACrE,MAAI,SAAS,QAAS,QAAO,CAAC;AAE9B,QAAM,UAAU;AAAA,IACf,EAAE,KAAK,SAAS,MAAM,KAAK,KAAK,UAAU,OAAO,CAAC,GAAG,GAAG,KAAK,IAAI,IAAI;AAAA,EACtE;AAIA,MAAI,EAAE,SAAS,cAAc,YAAY,YAAY;AACpD,YAAQ,QAAQ;AAAA,MACf,KAAK;AAAA,MACL,MAAM,KAAK,KAAK,UAAU,UAAU,CAAC,GAAG,GAAG,KAAK,IAAI;AAAA,IACrD,CAAC;AAAA,EACF;AACA,SAAO;AACR;","names":[]}