lazypock 0.11.0 → 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 +98 -122
- package/dist/{chunk-RYUQWFG2.js → chunk-HA22OVUR.js} +14 -3
- package/dist/chunk-HA22OVUR.js.map +1 -0
- package/dist/cli.cjs +12 -2
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.global.js +12 -2
- package/dist/cli.global.js.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/index.cjs +13 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -2
- package/dist/index.d.ts +8 -2
- package/dist/index.global.js +13 -2
- 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 +32 -3
- package/src/typegen.ts +2 -0
- package/src/types.ts +16 -2
- package/dist/chunk-RYUQWFG2.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
|
|
@@ -254,12 +275,15 @@ const posts = await postsSvc.getFullList({ expand: 'author' });
|
|
|
254
275
|
posts[0].expand?.author?.email; // ✓ typed, not unknown
|
|
255
276
|
```
|
|
256
277
|
|
|
257
|
-
- `filter` — `field op value` clauses with `= != ~ !~ > >= < <=` operators
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
`
|
|
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.
|
|
263
287
|
- `sort` — `field`, `-field` (desc), `+field`, or comma-separated. **Every**
|
|
264
288
|
token is validated.
|
|
265
289
|
- `expand` — comma-separated relation field names, including nested dot-paths
|
|
@@ -294,54 +318,21 @@ the result).
|
|
|
294
318
|
- The **untyped** client (`client.collection('posts')` without `typed<T>()`)
|
|
295
319
|
still accepts any string — suggestions kick in once the service is typed.
|
|
296
320
|
|
|
297
|
-
### CLI reference
|
|
298
|
-
|
|
299
|
-
```bash
|
|
300
|
-
lazypock [options]
|
|
301
|
-
|
|
302
|
-
Options:
|
|
303
|
-
--url <url> API base URL (or LAZYPOCK_URL)
|
|
304
|
-
--apikey <key> API key (or LAZYPOCK_API_KEY) — recommended, no login round-trip
|
|
305
|
-
--api-key <key> Deprecated alias for --apikey
|
|
306
|
-
--email <email> Superuser email (or LAZYPOCK_EMAIL)
|
|
307
|
-
--password <pw> Superuser password (or LAZYPOCK_PASSWORD)
|
|
308
|
-
--output <file> Output file (default: lazypock.types.ts)
|
|
309
|
-
--out <file> Deprecated alias for --output
|
|
310
|
-
--package <name> Package name to import (default: lazypock)
|
|
311
|
-
--skip-system Skip system collections
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
> **Note:** `lazypock-gen` is still available as a deprecated alias.
|
|
315
|
-
|
|
316
|
-
You must provide credentials one of two ways (or via the matching env vars):
|
|
317
|
-
|
|
318
|
-
1. `--apikey` / `LAZYPOCK_API_KEY` — scoped to collection listing, no login.
|
|
319
|
-
2. `--email` + `--password` / `LAZYPOCK_EMAIL` + `LAZYPOCK_PASSWORD` — superuser login.
|
|
320
|
-
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
|
|
324
321
|
## API Reference
|
|
325
322
|
|
|
326
|
-
### LazypockClient
|
|
323
|
+
### `LazypockClient`
|
|
327
324
|
|
|
328
325
|
The main client class.
|
|
329
326
|
|
|
330
327
|
#### Constructor Options
|
|
331
328
|
|
|
332
329
|
| Option | Type | Default | Description |
|
|
333
|
-
|
|
330
|
+
| --- | --- | --- | --- |
|
|
334
331
|
| `baseUrl` | `string` | required | API base URL (e.g. `http://localhost:4000/api`) |
|
|
335
332
|
| `storage` | `StorageAdapter` | `memoryStorage` | Custom storage adapter for token persistence |
|
|
336
333
|
| `authStore` | `AuthStore` | auto-created | Explicit auth store instance |
|
|
337
334
|
| `realtime` | `RealtimeService` | auto-created | Real-time service for WebSocket subscriptions |
|
|
338
335
|
|
|
339
|
-
#### Auto-Cancellation Methods
|
|
340
|
-
|
|
341
|
-
- `autoCancellation(enable)` — Globally enable/disable auto-cancellation of duplicated pending requests
|
|
342
|
-
- `cancelRequest(requestKey)` — Abort a single pending request by key (default `HTTP_METHOD + path`)
|
|
343
|
-
- `cancelAllRequests()` — Abort all pending requests
|
|
344
|
-
|
|
345
336
|
#### Authentication Methods
|
|
346
337
|
|
|
347
338
|
- `login(email, password, collection?)` — Login as superuser or auth collection user
|
|
@@ -352,7 +343,13 @@ The main client class.
|
|
|
352
343
|
- `logout()` — Clear auth state
|
|
353
344
|
- `me(options?)` — Get current superuser profile
|
|
354
345
|
|
|
355
|
-
####
|
|
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`)
|
|
356
353
|
|
|
357
354
|
PocketBase-style service for the collections themselves (admin):
|
|
358
355
|
|
|
@@ -364,14 +361,17 @@ PocketBase-style service for the collections themselves (admin):
|
|
|
364
361
|
- `collections.delete(id, options?)` — Delete collection
|
|
365
362
|
- `collections.subscribe(cb)` — Subscribe to collection create/update/delete events (returns unsubscribe fn)
|
|
366
363
|
- `collections.unsubscribe()` — Unsubscribe from registry events
|
|
367
|
-
|
|
364
|
+
|
|
365
|
+
### File Operations
|
|
368
366
|
|
|
369
367
|
- `files.upload(file, filename?, options?, meta?)` — Upload a file
|
|
370
368
|
- `files.getUrl(fileId)` — Get file metadata
|
|
371
369
|
- `files.delete(fileId, options?)` — Delete a file
|
|
372
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`)
|
|
373
373
|
|
|
374
|
-
|
|
374
|
+
### Realtime
|
|
375
375
|
|
|
376
376
|
- `realtime.connect(opts)` — Connect to WebSocket
|
|
377
377
|
- `realtime.disconnect()` — Disconnect
|
|
@@ -459,10 +459,7 @@ interface RequestOptions {
|
|
|
459
459
|
|
|
460
460
|
## Auto Cancellation
|
|
461
461
|
|
|
462
|
-
The SDK auto-cancels duplicated pending requests for you (PocketBase-compatible
|
|
463
|
-
behaviour). When a new request is issued with the same request key as a
|
|
464
|
-
still-pending request, the previous one is aborted — only the last request
|
|
465
|
-
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:
|
|
466
463
|
|
|
467
464
|
```typescript
|
|
468
465
|
// Only the last call will execute; the first two are auto-cancelled
|
|
@@ -471,9 +468,7 @@ await client.collection('posts').getList(2, 20); // cancelled
|
|
|
471
468
|
await client.collection('posts').getList(3, 20); // executed
|
|
472
469
|
```
|
|
473
470
|
|
|
474
|
-
By default the request key is `HTTP_METHOD + path` (e.g. `"GET /api/posts?page=1"`), so
|
|
475
|
-
duplicate calls with identical URLs cancel each other. Cancelled requests reject
|
|
476
|
-
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`:
|
|
477
472
|
|
|
478
473
|
```typescript
|
|
479
474
|
try {
|
|
@@ -485,10 +480,9 @@ try {
|
|
|
485
480
|
}
|
|
486
481
|
```
|
|
487
482
|
|
|
488
|
-
|
|
483
|
+
### Per-request control
|
|
489
484
|
|
|
490
|
-
Pass `requestKey` in the request options to customize the key, or disable
|
|
491
|
-
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:
|
|
492
486
|
|
|
493
487
|
```typescript
|
|
494
488
|
await client.collection('posts').getList(1, 20, { requestKey: 'my-list' }); // cancelled
|
|
@@ -498,7 +492,7 @@ await client.collection('posts').getList(1, 20, { requestKey: null }); // exec
|
|
|
498
492
|
await client.collection('posts').getList(1, 20, { requestKey: null }); // executed
|
|
499
493
|
```
|
|
500
494
|
|
|
501
|
-
|
|
495
|
+
### Global control
|
|
502
496
|
|
|
503
497
|
```typescript
|
|
504
498
|
// Disable auto-cancellation globally
|
|
@@ -509,13 +503,9 @@ client.cancelRequest('GET /api/posts?page=1');
|
|
|
509
503
|
client.cancelAllRequests();
|
|
510
504
|
```
|
|
511
505
|
|
|
512
|
-
|
|
506
|
+
### Single-flight dedup (`getFullList`)
|
|
513
507
|
|
|
514
|
-
`getFullList()` (and `collections.getFullList()`) are **single-flight**: concurrent
|
|
515
|
-
calls with the same effective options share one in-flight request instead of
|
|
516
|
-
firing duplicates. This means the common pattern below results in **one**
|
|
517
|
-
network request, and **both** callers resolve with the same data — no abort
|
|
518
|
-
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:
|
|
519
509
|
|
|
520
510
|
```typescript
|
|
521
511
|
const [a, b] = await Promise.all([
|
|
@@ -525,13 +515,9 @@ const [a, b] = await Promise.all([
|
|
|
525
515
|
// one GET fired; a === b
|
|
526
516
|
```
|
|
527
517
|
|
|
528
|
-
Calls with **different** options (e.g. different `sort`/`filter`) are still
|
|
529
|
-
distinct requests. Multi-page fetches continue to work normally — each page
|
|
530
|
-
request is unique (page number is part of the URL), so pages never cancel each
|
|
531
|
-
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.
|
|
532
519
|
|
|
533
|
-
The underlying `singleFlight` option is also available on any request when you
|
|
534
|
-
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:
|
|
535
521
|
|
|
536
522
|
```typescript
|
|
537
523
|
await client.collection('posts').getList(1, 20, { singleFlight: true });
|
|
@@ -634,11 +620,7 @@ client.collection('private_feed').subscribe('*', (e) => { ... });
|
|
|
634
620
|
|
|
635
621
|
### Anonymous / rule-based realtime
|
|
636
622
|
|
|
637
|
-
Realtime subscriptions honor your API **and list rules** — matching PocketBase
|
|
638
|
-
behavior. This means **non-logged-in users can subscribe** to collections whose
|
|
639
|
-
list rules are public (empty `""` string) or anon-friendly
|
|
640
|
-
(`@request.auth.*` filters). The SDK auto-connects the WebSocket on first use,
|
|
641
|
-
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:
|
|
642
624
|
|
|
643
625
|
```typescript
|
|
644
626
|
// Works without logging in, as long as the collection's list rule allows it
|
|
@@ -682,12 +664,6 @@ There is no manual `workflow_dispatch` step and no local `npm publish`.
|
|
|
682
664
|
- `feat(...)` commits → minor (`0.8.2` → `0.9.0`)
|
|
683
665
|
- a `BREAKING CHANGE:` footer in any commit body → major (`0.8.2` → `1.0.0`)
|
|
684
666
|
|
|
685
|
-
### One-time setup
|
|
686
|
-
|
|
687
|
-
- Add an npm access token (Automation or Publish scope, from
|
|
688
|
-
<<https://www.npmjs.com/settings/><you>/tokens>) as the repo secret
|
|
689
|
-
**`NPM_TOKEN`** under Settings → Secrets and variables → Actions.
|
|
690
|
-
|
|
691
667
|
## License
|
|
692
668
|
|
|
693
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
|
}
|
|
@@ -130,7 +139,7 @@ function generateTypes(collections, options = {}) {
|
|
|
130
139
|
for (const coll of filtered) {
|
|
131
140
|
const typeName = collectionTypeName(coll.name);
|
|
132
141
|
const fields = coll.fields ?? [];
|
|
133
|
-
const lines = fields.map((f) => memberLine(f)).filter((l) => l !== "");
|
|
142
|
+
const lines = fields.map((f) => memberLine(f, includeBaseFields)).filter((l) => l !== "");
|
|
134
143
|
const body = lines.join("\n");
|
|
135
144
|
sections.push(
|
|
136
145
|
`export interface ${typeName}Record${renderInterface({
|
|
@@ -156,6 +165,7 @@ function generateTypes(collections, options = {}) {
|
|
|
156
165
|
for (const f of fields) {
|
|
157
166
|
if (f.type === "password") continue;
|
|
158
167
|
const key = fieldKey(f.name);
|
|
168
|
+
if (includeBaseFields && BASE_RECORD_KEYS.has(key)) continue;
|
|
159
169
|
const type = fieldTypeScriptType(f);
|
|
160
170
|
if (type === "never") continue;
|
|
161
171
|
queryLines.push(` ${JSON.stringify(key)}?: ${type};`);
|
|
@@ -300,9 +310,10 @@ function renderInterface(opts) {
|
|
|
300
310
|
${opts.body}
|
|
301
311
|
}`;
|
|
302
312
|
}
|
|
303
|
-
function memberLine(f) {
|
|
313
|
+
function memberLine(f, skipBaseKeys = false) {
|
|
304
314
|
if (f.hidden) return "";
|
|
305
315
|
const key = fieldKey(f.name);
|
|
316
|
+
if (skipBaseKeys && BASE_RECORD_KEYS.has(key)) return "";
|
|
306
317
|
const req = f.required || f.type === "password" ? "" : "?";
|
|
307
318
|
const type = fieldTypeScriptType(f);
|
|
308
319
|
if (type === "never") return "";
|
|
@@ -334,4 +345,4 @@ export {
|
|
|
334
345
|
collectionTypeName,
|
|
335
346
|
generateTypes
|
|
336
347
|
};
|
|
337
|
-
//# 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":[]}
|
package/dist/cli.cjs
CHANGED
|
@@ -15,6 +15,7 @@ function fieldTypeScriptType(field, fallback = "unknown") {
|
|
|
15
15
|
case "editor":
|
|
16
16
|
case "date":
|
|
17
17
|
case "datetime":
|
|
18
|
+
case "autodate":
|
|
18
19
|
return "string";
|
|
19
20
|
case "number":
|
|
20
21
|
return "number";
|
|
@@ -51,6 +52,13 @@ function fieldTypeScriptType(field, fallback = "unknown") {
|
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
// src/codegen.ts
|
|
55
|
+
var BASE_RECORD_KEYS = /* @__PURE__ */ new Set([
|
|
56
|
+
"id",
|
|
57
|
+
"collectionId",
|
|
58
|
+
"collectionName",
|
|
59
|
+
"created",
|
|
60
|
+
"updated"
|
|
61
|
+
]);
|
|
54
62
|
function collectionTypeName(name) {
|
|
55
63
|
return name.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("").replace(/^[0-9]/, "_$&");
|
|
56
64
|
}
|
|
@@ -85,7 +93,7 @@ function generateTypes(collections, options = {}) {
|
|
|
85
93
|
for (const coll of filtered) {
|
|
86
94
|
const typeName = collectionTypeName(coll.name);
|
|
87
95
|
const fields = coll.fields ?? [];
|
|
88
|
-
const lines = fields.map((f) => memberLine(f)).filter((l) => l !== "");
|
|
96
|
+
const lines = fields.map((f) => memberLine(f, includeBaseFields)).filter((l) => l !== "");
|
|
89
97
|
const body = lines.join("\n");
|
|
90
98
|
sections.push(
|
|
91
99
|
`export interface ${typeName}Record${renderInterface({
|
|
@@ -111,6 +119,7 @@ function generateTypes(collections, options = {}) {
|
|
|
111
119
|
for (const f of fields) {
|
|
112
120
|
if (f.type === "password") continue;
|
|
113
121
|
const key = fieldKey(f.name);
|
|
122
|
+
if (includeBaseFields && BASE_RECORD_KEYS.has(key)) continue;
|
|
114
123
|
const type = fieldTypeScriptType(f);
|
|
115
124
|
if (type === "never") continue;
|
|
116
125
|
queryLines.push(` ${JSON.stringify(key)}?: ${type};`);
|
|
@@ -255,9 +264,10 @@ function renderInterface(opts) {
|
|
|
255
264
|
${opts.body}
|
|
256
265
|
}`;
|
|
257
266
|
}
|
|
258
|
-
function memberLine(f) {
|
|
267
|
+
function memberLine(f, skipBaseKeys = false) {
|
|
259
268
|
if (f.hidden) return "";
|
|
260
269
|
const key = fieldKey(f.name);
|
|
270
|
+
if (skipBaseKeys && BASE_RECORD_KEYS.has(key)) return "";
|
|
261
271
|
const req = f.required || f.type === "password" ? "" : "?";
|
|
262
272
|
const type = fieldTypeScriptType(f);
|
|
263
273
|
if (type === "never") return "";
|