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 +117 -127
- package/dist/{chunk-LVEOMG6A.js → chunk-HA22OVUR.js} +51 -9
- package/dist/chunk-HA22OVUR.js.map +1 -0
- package/dist/cli.cjs +49 -8
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.global.js +49 -8
- package/dist/cli.global.js.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/index.cjs +50 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +35 -14
- package/dist/index.d.ts +35 -14
- package/dist/index.global.js +50 -8
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/codegen.ts +98 -9
- package/src/collection.ts +22 -13
- package/src/typegen.ts +2 -0
- package/src/types.ts +31 -7
- package/dist/chunk-LVEOMG6A.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
#
|
|
1
|
+
# lazypock
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/lazypock)
|
|
4
|
+
[](https://www.npmjs.com/package/lazypock)
|
|
5
|
+
[](https://github.com/gnuzd/lazypock-ts/blob/main/LICENSE)
|
|
6
|
+
[](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
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
>
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
178
|
+
## Field Projection (`select`) & Query Suggestions
|
|
154
179
|
|
|
155
|
-
|
|
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
|
|
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
|
-
|
|
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; // ✓
|
|
275
|
+
posts[0].expand?.author?.email; // ✓ typed, not unknown
|
|
254
276
|
```
|
|
255
277
|
|
|
256
|
-
- `filter` — `field op value` clauses with `= != ~ !~ > >= < <=` operators
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
`
|
|
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).
|
|
272
|
-
target collection
|
|
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
|
|
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
|
-
####
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
//
|
|
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-
|
|
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":"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","names":[]}
|