lazypock 0.8.3 → 0.9.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 +104 -20
- package/dist/{chunk-V2XSEOWF.js → chunk-CGQVJ7TT.js} +24 -7
- package/dist/chunk-CGQVJ7TT.js.map +1 -0
- package/dist/cli.cjs +23 -6
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.global.js +23 -6
- package/dist/cli.global.js.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/index.cjs +110 -35
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +65 -8
- package/dist/index.d.ts +65 -8
- package/dist/index.global.js +110 -35
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +88 -30
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/codegen.ts +38 -11
- package/src/collection.ts +77 -9
- package/src/index.ts +1 -1
- package/src/lazypock.ts +10 -0
- package/src/realtime.ts +79 -11
- package/dist/chunk-V2XSEOWF.js.map +0 -1
package/src/codegen.ts
CHANGED
|
@@ -90,9 +90,15 @@ export function generateTypes(
|
|
|
90
90
|
// intentionally includes password fields (and hidden fields) that are
|
|
91
91
|
// excluded from the read model — the server accepts them on write,
|
|
92
92
|
// hashes passwords, and never returns them in responses.
|
|
93
|
-
const createLines =
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
const createLines: string[] = [];
|
|
94
|
+
const seenKeys = new Set<string>();
|
|
95
|
+
for (const f of fields) {
|
|
96
|
+
for (const { key, line } of createDataMemberLines(f)) {
|
|
97
|
+
if (seenKeys.has(key)) continue;
|
|
98
|
+
seenKeys.add(key);
|
|
99
|
+
createLines.push(line);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
96
102
|
sections.push(
|
|
97
103
|
`export interface ${typeName}CreateData${renderInterface({
|
|
98
104
|
body: createLines.join("\n"),
|
|
@@ -227,28 +233,49 @@ function memberLine(f: SchemaField): string {
|
|
|
227
233
|
}
|
|
228
234
|
|
|
229
235
|
/**
|
|
230
|
-
* Render
|
|
236
|
+
* Render the write-only create-data members for a field.
|
|
231
237
|
*
|
|
232
238
|
* Unlike the read model, create/update data includes password fields (sent as
|
|
233
239
|
* plain strings — the server hashes them) and hidden fields: the server
|
|
234
240
|
* accepts them on write and never returns them in responses.
|
|
235
241
|
*
|
|
242
|
+
* Password fields are exposed under the canonical PocketBase API key
|
|
243
|
+
* `password` (the server accepts `password` for any single password field and
|
|
244
|
+
* hashes it into the backing column, e.g. `password_hash`), with the raw
|
|
245
|
+
* metadata name kept as a backward-compat alias.
|
|
246
|
+
*
|
|
236
247
|
* A field is only marked required when it actually needs a client value:
|
|
237
248
|
* fields with a server-side default (`options.defaultValue`) and the auth
|
|
238
249
|
* system fields `verified` / `emailVisibility` (the server defaults them to
|
|
239
250
|
* false/true even though the snapshot doesn't carry `defaultValue`) stay
|
|
240
|
-
* optional, so `create({ email,
|
|
241
|
-
* callers to send values the server would fill in anyway.
|
|
251
|
+
* optional, so `create({ email, password })` typechecks without forcing
|
|
252
|
+
* callers to send values the server would fill in anyway. Password fields
|
|
253
|
+
* are optional (accounts may exist without a password, e.g. OAuth-only).
|
|
242
254
|
*/
|
|
243
|
-
function
|
|
255
|
+
function createDataMemberLines(
|
|
256
|
+
f: SchemaField,
|
|
257
|
+
): { key: string; line: string }[] {
|
|
244
258
|
// Autodate columns are server-managed; never writable.
|
|
245
|
-
if (f.type === "autodate") return
|
|
246
|
-
const
|
|
259
|
+
if (f.type === "autodate") return [];
|
|
260
|
+
const rawName = fieldKey(f.name);
|
|
247
261
|
const serverDefaulted =
|
|
248
262
|
f.options?.defaultValue !== undefined ||
|
|
249
263
|
(f.system && (f.name === "verified" || f.name === "emailVisibility"));
|
|
250
264
|
const req = f.required && !serverDefaulted ? "" : "?";
|
|
251
265
|
const type = f.type === "password" ? "string" : fieldTypeScriptType(f);
|
|
252
|
-
if (type === "never") return
|
|
253
|
-
|
|
266
|
+
if (type === "never") return [];
|
|
267
|
+
|
|
268
|
+
const members = [
|
|
269
|
+
{ key: rawName, line: ` ${JSON.stringify(rawName)}${req}: ${type};` },
|
|
270
|
+
];
|
|
271
|
+
// Write-only password: canonical key is `password` (PocketBase parity) —
|
|
272
|
+
// the server accepts `password` for any single password field and hashes
|
|
273
|
+
// it. The raw metadata name (e.g. `password_hash`) stays as a compat alias.
|
|
274
|
+
if (f.type === "password" && rawName !== "password") {
|
|
275
|
+
members.unshift({
|
|
276
|
+
key: "password",
|
|
277
|
+
line: ` ${JSON.stringify("password")}${req}: ${type};`,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
return members;
|
|
254
281
|
}
|
package/src/collection.ts
CHANGED
|
@@ -60,6 +60,23 @@ export interface RealtimeMessage {
|
|
|
60
60
|
/** Subscription callback for a collection's realtime events. */
|
|
61
61
|
export type RealtimeCallback = (e: RealtimeMessage) => void;
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Options for {@link CollectionService.subscribe} (PocketBase-compatible).
|
|
65
|
+
*
|
|
66
|
+
* `expand` and any extra keys are forwarded to the server channel join
|
|
67
|
+
* payload (available to `onRealtimeSubscribeRequest` hooks). `headers` is
|
|
68
|
+
* accepted for PocketBase signature compatibility but has no effect over
|
|
69
|
+
* WebSocket — it is not forwarded.
|
|
70
|
+
*/
|
|
71
|
+
export interface RealtimeSubscribeOptions {
|
|
72
|
+
/** Fields to expand on the subscribed records (server join payload). */
|
|
73
|
+
expand?: string;
|
|
74
|
+
/** Accepted for PocketBase parity; not sent over the WebSocket. */
|
|
75
|
+
headers?: Record<string, string>;
|
|
76
|
+
/** Any extra keys are forwarded verbatim to the server join payload. */
|
|
77
|
+
[key: string]: unknown;
|
|
78
|
+
}
|
|
79
|
+
|
|
63
80
|
/** Raw event passed by the low-level realtime service. */
|
|
64
81
|
interface RealtimeEventLike {
|
|
65
82
|
event: string;
|
|
@@ -505,23 +522,64 @@ export class CollectionService<T = ApiRecord, TData = never> {
|
|
|
505
522
|
|
|
506
523
|
/**
|
|
507
524
|
* Subscribe to realtime changes for this collection.
|
|
508
|
-
* The event's `action` is one of `"create" | "update" | "delete"
|
|
525
|
+
* The event's `action` is one of `"create" | "update" | "delete"` and the
|
|
526
|
+
* callback always receives the **full record** (all fields, regardless of
|
|
527
|
+
* any `select()` projection on the service).
|
|
509
528
|
*
|
|
510
529
|
* Access is governed by the collection's `listRule` (PocketBase semantics):
|
|
511
530
|
* public collections allow anonymous subscriptions; other collections
|
|
512
531
|
* require a matching logged-in user or superuser.
|
|
513
532
|
*
|
|
514
|
-
*
|
|
515
|
-
*
|
|
533
|
+
* PocketBase-compatible argument forms:
|
|
534
|
+
* - `subscribe(cb)` — all records of the collection
|
|
535
|
+
* - `subscribe('*', cb)` — all records (explicit wildcard)
|
|
536
|
+
* - `subscribe('RECORD_ID', cb)` — a single record
|
|
537
|
+
* - `subscribe('*' | 'RECORD_ID', cb, options)` — with join options
|
|
538
|
+
*
|
|
539
|
+
* Legacy form (callback first) is still accepted: `subscribe(cb, recordId)`.
|
|
540
|
+
*
|
|
516
541
|
* @returns A function that unsubscribes this callback.
|
|
517
542
|
*/
|
|
518
|
-
subscribe(callback: RealtimeCallback
|
|
543
|
+
subscribe(callback: RealtimeCallback): () => void;
|
|
544
|
+
subscribe(
|
|
545
|
+
recordId: string,
|
|
546
|
+
callback: RealtimeCallback,
|
|
547
|
+
options?: RealtimeSubscribeOptions,
|
|
548
|
+
): () => void;
|
|
549
|
+
/** @deprecated Use `subscribe(recordId, callback)` — PocketBase order. */
|
|
550
|
+
subscribe(callback: RealtimeCallback, recordId?: string): () => void;
|
|
551
|
+
subscribe(
|
|
552
|
+
topicOrCallback: string | RealtimeCallback,
|
|
553
|
+
maybeCallback?: RealtimeCallback | string,
|
|
554
|
+
options?: RealtimeSubscribeOptions,
|
|
555
|
+
): () => void {
|
|
519
556
|
if (!this.realtime) {
|
|
520
557
|
console.warn("[lazypock] No realtime service configured.");
|
|
521
558
|
return () => {};
|
|
522
559
|
}
|
|
560
|
+
|
|
561
|
+
// Normalise PocketBase-style args; also accept the legacy callback-first
|
|
562
|
+
// form (subscribe(cb, recordId)) for backward compatibility.
|
|
563
|
+
let recordId: string | undefined;
|
|
564
|
+
let callback: RealtimeCallback;
|
|
565
|
+
let joinPayload: Record<string, unknown> | undefined;
|
|
566
|
+
|
|
567
|
+
if (typeof topicOrCallback === "function") {
|
|
568
|
+
callback = topicOrCallback;
|
|
569
|
+
if (typeof maybeCallback === "string") recordId = maybeCallback;
|
|
570
|
+
} else {
|
|
571
|
+
callback = maybeCallback as RealtimeCallback;
|
|
572
|
+
recordId = topicOrCallback === "*" ? undefined : topicOrCallback;
|
|
573
|
+
// Forward subscribe options (minus HTTP-only headers) to the server
|
|
574
|
+
// channel join payload.
|
|
575
|
+
const { headers: _ignored, ...rest } = options ?? {};
|
|
576
|
+
if (Object.keys(rest).length > 0) joinPayload = rest;
|
|
577
|
+
}
|
|
578
|
+
|
|
523
579
|
const topic =
|
|
524
|
-
"collection:" +
|
|
580
|
+
"collection:" +
|
|
581
|
+
this.collectionName +
|
|
582
|
+
(recordId ? ":" + recordId : "");
|
|
525
583
|
const handler = (raw: RealtimeEventLike) => {
|
|
526
584
|
const record = (raw.payload?.["record"] ?? {}) as Record<string, unknown>;
|
|
527
585
|
callback({
|
|
@@ -531,17 +589,27 @@ export class CollectionService<T = ApiRecord, TData = never> {
|
|
|
531
589
|
});
|
|
532
590
|
};
|
|
533
591
|
this.realtime.ensureConnected();
|
|
534
|
-
this.realtime.subscribe(topic, handler as never);
|
|
592
|
+
this.realtime.subscribe(topic, handler as never, joinPayload);
|
|
535
593
|
return () => this.realtime?.unsubscribe(topic, handler as never);
|
|
536
594
|
}
|
|
537
595
|
|
|
538
596
|
/**
|
|
539
|
-
* Unsubscribe
|
|
540
|
-
*
|
|
597
|
+
* Unsubscribe from realtime changes (PocketBase-compatible):
|
|
598
|
+
* - `unsubscribe()` — remove **all** subscriptions of this collection
|
|
599
|
+
* - `unsubscribe('*')` — remove wildcard subscriptions
|
|
600
|
+
* - `unsubscribe('RECORD_ID')` — remove that record's subscriptions
|
|
541
601
|
*/
|
|
542
602
|
unsubscribe(recordId?: string): void {
|
|
603
|
+
if (recordId === undefined) {
|
|
604
|
+
this.realtime?.unsubscribeByPrefix(
|
|
605
|
+
"collection:" + this.collectionName,
|
|
606
|
+
);
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
543
609
|
const topic =
|
|
544
|
-
"collection:" +
|
|
610
|
+
"collection:" +
|
|
611
|
+
this.collectionName +
|
|
612
|
+
(recordId === "*" ? "" : ":" + recordId);
|
|
545
613
|
this.realtime?.unsubscribe(topic);
|
|
546
614
|
}
|
|
547
615
|
|
package/src/index.ts
CHANGED
|
@@ -56,5 +56,5 @@ export type {
|
|
|
56
56
|
LazypockClientOptions,
|
|
57
57
|
} from "./lazypock";
|
|
58
58
|
export type { LazypockCollections } from "./client";
|
|
59
|
-
export type { RealtimeMessage, RealtimeCallback } from "./collection";
|
|
59
|
+
export type { RealtimeMessage, RealtimeCallback, RealtimeSubscribeOptions } from "./collection";
|
|
60
60
|
export type { CollectionsMessage } from "./collections";
|
package/src/lazypock.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
type FieldKey,
|
|
29
29
|
} from "./types";
|
|
30
30
|
import { RealtimeService, wsUrlFromBaseUrl } from "./realtime";
|
|
31
|
+
import type { RealtimeConnectOpts, RealtimeTokenProvider } from "./realtime";
|
|
31
32
|
import {
|
|
32
33
|
FilesService,
|
|
33
34
|
getFileUrl,
|
|
@@ -69,6 +70,9 @@ export type {
|
|
|
69
70
|
SystemFields,
|
|
70
71
|
RequestOptions,
|
|
71
72
|
FileRecord,
|
|
73
|
+
// realtime
|
|
74
|
+
RealtimeConnectOpts,
|
|
75
|
+
RealtimeTokenProvider,
|
|
72
76
|
CollectionSchema,
|
|
73
77
|
SchemaField,
|
|
74
78
|
// schema-driven query typing
|
|
@@ -154,6 +158,12 @@ export class LazypockClient {
|
|
|
154
158
|
if (!options.realtime) {
|
|
155
159
|
this.realtime.setUrl(wsUrlFromBaseUrl(baseUrl));
|
|
156
160
|
}
|
|
161
|
+
// Keep the realtime socket authenticated with the current auth token
|
|
162
|
+
// and reconnect whenever auth changes (login/logout/token refresh) —
|
|
163
|
+
// PocketBase parity. Reconnect is a no-op until something subscribes.
|
|
164
|
+
this.realtime.setTokenProvider(() => this.authStore.token);
|
|
165
|
+
this.authStore.onChange(() => this.realtime.refresh());
|
|
166
|
+
this.authReady.then(() => this.realtime.refresh());
|
|
157
167
|
this.files = new FilesService(this.http);
|
|
158
168
|
this.collections = new CollectionsService(this.http, this.realtime);
|
|
159
169
|
if (options.types?.schemas) {
|
package/src/realtime.ts
CHANGED
|
@@ -16,6 +16,8 @@ interface RealtimeEvent {
|
|
|
16
16
|
interface SubEntry {
|
|
17
17
|
topic: string;
|
|
18
18
|
callback: (e: RealtimeEvent) => void;
|
|
19
|
+
/** Optional payload forwarded with the channel join (e.g. expand). */
|
|
20
|
+
joinPayload?: Record<string, unknown>;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
export type RealtimeConnectOpts = {
|
|
@@ -25,6 +27,13 @@ export type RealtimeConnectOpts = {
|
|
|
25
27
|
token?: string;
|
|
26
28
|
};
|
|
27
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Provides the current auth token at connect/reconnect time.
|
|
32
|
+
* Wired by {@link LazypockClient} from its `authStore` so the socket is
|
|
33
|
+
* always authenticated with the latest token (PocketBase parity).
|
|
34
|
+
*/
|
|
35
|
+
export type RealtimeTokenProvider = () => string | undefined;
|
|
36
|
+
|
|
28
37
|
/**
|
|
29
38
|
* Derive a WebSocket URL from an HTTP base URL.
|
|
30
39
|
* http://localhost:4000/api → ws://localhost:4000/socket/websocket
|
|
@@ -67,6 +76,7 @@ export class RealtimeService {
|
|
|
67
76
|
|
|
68
77
|
private url: string = "";
|
|
69
78
|
private token: string | undefined;
|
|
79
|
+
private tokenProvider: RealtimeTokenProvider | null = null;
|
|
70
80
|
|
|
71
81
|
/** Whether the WebSocket is currently open. */
|
|
72
82
|
get isOpen(): boolean {
|
|
@@ -94,6 +104,35 @@ export class RealtimeService {
|
|
|
94
104
|
this.url = url;
|
|
95
105
|
}
|
|
96
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Register a token provider consulted at every (re)connect.
|
|
109
|
+
* When set, it takes precedence over the token passed to {@link connect}.
|
|
110
|
+
*/
|
|
111
|
+
setTokenProvider(provider: RealtimeTokenProvider): void {
|
|
112
|
+
this.tokenProvider = provider;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Reconnect the socket immediately with the current token.
|
|
117
|
+
* Called by the SDK when auth changes (login/logout/token refresh) so
|
|
118
|
+
* private-channel joins are authorized with the new credentials. No-op
|
|
119
|
+
* when the socket has never been opened and nothing is subscribed.
|
|
120
|
+
*/
|
|
121
|
+
refresh(): void {
|
|
122
|
+
if (typeof WebSocket === "undefined") return;
|
|
123
|
+
if (!this.ws && this.subscriptions.size === 0) return;
|
|
124
|
+
this.clearReconnectTimer();
|
|
125
|
+
const ws = this.ws;
|
|
126
|
+
this.ws = null;
|
|
127
|
+
if (ws) {
|
|
128
|
+
// Suppress the auto-reconnect path for this intentional close.
|
|
129
|
+
ws.onclose = null;
|
|
130
|
+
ws.close();
|
|
131
|
+
}
|
|
132
|
+
this.reconnectAttempt = 0;
|
|
133
|
+
this.doConnect();
|
|
134
|
+
}
|
|
135
|
+
|
|
97
136
|
/*
|
|
98
137
|
* Ensure the socket is connected, then subscribe.
|
|
99
138
|
* Used by collection-level convenience wrappers so a connection is opened
|
|
@@ -120,16 +159,36 @@ export class RealtimeService {
|
|
|
120
159
|
}
|
|
121
160
|
|
|
122
161
|
/**
|
|
123
|
-
* Subscribe to a topic (e.g. "collection:posts" or "
|
|
162
|
+
* Subscribe to a topic (e.g. "collection:posts" or "custom:chat-room").
|
|
124
163
|
* The backend Channel authorizes via listRule on join.
|
|
164
|
+
*
|
|
165
|
+
* @param joinPayload Optional payload forwarded with the channel join
|
|
166
|
+
* (available to the server's join callback / hooks, e.g. `expand`).
|
|
125
167
|
*/
|
|
126
|
-
subscribe(
|
|
168
|
+
subscribe(
|
|
169
|
+
topic: string,
|
|
170
|
+
callback: (e: RealtimeEvent) => void,
|
|
171
|
+
joinPayload?: Record<string, unknown>,
|
|
172
|
+
): void {
|
|
127
173
|
const subs = this.subscriptions.get(topic) || [];
|
|
128
|
-
subs.push({ topic, callback });
|
|
174
|
+
subs.push({ topic, callback, joinPayload });
|
|
129
175
|
this.subscriptions.set(topic, subs);
|
|
130
176
|
|
|
131
177
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
132
|
-
this.joinTopic(topic);
|
|
178
|
+
this.joinTopic(topic, joinPayload);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Remove all subscriptions for topics under `prefix` (the topic itself or
|
|
184
|
+
* any topic starting with `prefix + ":"`). Used by collection-level
|
|
185
|
+
* `unsubscribe()` to drop every subscription of a collection.
|
|
186
|
+
*/
|
|
187
|
+
unsubscribeByPrefix(prefix: string): void {
|
|
188
|
+
for (const topic of [...this.subscriptions.keys()]) {
|
|
189
|
+
if (topic === prefix || topic.startsWith(prefix + ":")) {
|
|
190
|
+
this.subscriptions.delete(topic);
|
|
191
|
+
}
|
|
133
192
|
}
|
|
134
193
|
}
|
|
135
194
|
|
|
@@ -152,9 +211,14 @@ export class RealtimeService {
|
|
|
152
211
|
}
|
|
153
212
|
|
|
154
213
|
private resubscribeAll(): void {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
214
|
+
const seen = new Set<string>();
|
|
215
|
+
for (const entries of this.subscriptions.values()) {
|
|
216
|
+
for (const entry of entries) {
|
|
217
|
+
if (seen.has(entry.topic)) continue;
|
|
218
|
+
seen.add(entry.topic);
|
|
219
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
220
|
+
this.joinTopic(entry.topic, entry.joinPayload);
|
|
221
|
+
}
|
|
158
222
|
}
|
|
159
223
|
}
|
|
160
224
|
}
|
|
@@ -168,11 +232,12 @@ export class RealtimeService {
|
|
|
168
232
|
}
|
|
169
233
|
|
|
170
234
|
let url = this.url;
|
|
171
|
-
|
|
235
|
+
const token = this.tokenProvider ? this.tokenProvider() : this.token;
|
|
236
|
+
if (token) {
|
|
172
237
|
url +=
|
|
173
238
|
(url.includes("?") ? "&" : "?") +
|
|
174
239
|
"token=" +
|
|
175
|
-
encodeURIComponent(
|
|
240
|
+
encodeURIComponent(token);
|
|
176
241
|
}
|
|
177
242
|
|
|
178
243
|
this.ws = new WebSocket(url);
|
|
@@ -232,13 +297,16 @@ export class RealtimeService {
|
|
|
232
297
|
}
|
|
233
298
|
}
|
|
234
299
|
|
|
235
|
-
private joinTopic(
|
|
300
|
+
private joinTopic(
|
|
301
|
+
topic: string,
|
|
302
|
+
joinPayload?: Record<string, unknown>,
|
|
303
|
+
): void {
|
|
236
304
|
const ref = this.nextRef();
|
|
237
305
|
// Phoenix V1 JSON Serializer expects a JSON object, not an array
|
|
238
306
|
const msg = JSON.stringify({
|
|
239
307
|
topic: topic,
|
|
240
308
|
event: "phx_join",
|
|
241
|
-
payload: {},
|
|
309
|
+
payload: joinPayload ?? {},
|
|
242
310
|
ref: ref,
|
|
243
311
|
});
|
|
244
312
|
this.ws?.send(msg);
|
|
@@ -1 +0,0 @@
|
|
|
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\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 \"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/** 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// 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.map((f) => memberLine(f)).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 = fields\n\t\t\t.map((f) => createDataMemberLine(f))\n\t\t\t.filter((l) => l !== \"\");\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\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// 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 // K extends keyof LazypockCollections (rather than a generic string with a\n // conditional return) so the IDE suggests collection names and unknown names\n // are rejected at compile time:\n // client.collection(\"posts\") // suggested + typed\n // client.collection(\"nope\") // TS error\n //\n // create()/update() take the collection's *CreateData — the read model\n // omits password/hidden fields, but the write model carries them.\n // T extends string (rather than keyof LazypockCollections) with a conditional\n // return type, so the IDE suggests collection names AND unknown/dynamic names\n // still resolve to the untyped service — a studio that manages\n // user-created collections must be able to call collection(someString).\n //\n // create()/update() take the collection's *CreateData — the read model\n // omits password/hidden fields, but the write model carries them.\n override collection<T extends string>(name: T): T extends keyof LazypockCollections\n ? CollectionService<LazypockCollections[T], LazypockCreateData[T]>\n : CollectionService<unknown> {\n return super.collection(name) as T extends keyof LazypockCollections\n ? CollectionService<LazypockCollections[T], LazypockCreateData[T]>\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/** Render a single interface member line for a field. */\nfunction memberLine(f: SchemaField): 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\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 a write-only create-data member 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 * 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_hash })` typechecks without forcing\n * callers to send values the server would fill in anyway.\n */\nfunction createDataMemberLine(f: SchemaField): string {\n\t// Autodate columns are server-managed; never writable.\n\tif (f.type === \"autodate\") return \"\";\n\tconst key = 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\treturn ` ${JSON.stringify(key)}${req}: ${type};`;\n}\n"],"mappings":"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","names":[]}
|