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/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 = fields
94
- .map((f) => createDataMemberLine(f))
95
- .filter((l) => l !== "");
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 a write-only create-data member for a field.
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, password_hash })` typechecks without forcing
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 createDataMemberLine(f: SchemaField): string {
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 key = fieldKey(f.name);
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
- return ` ${JSON.stringify(key)}${req}: ${type};`;
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
- * @param callback Received on every record change.
515
- * @param recordId Optional — subscribe to a single record instead of `*`.
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, recordId?: string): () => void {
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:" + this.collectionName + (recordId ? ":" + recordId : "");
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 all callbacks from this collection (or a specific record).
540
- * @param recordId Optional record id; omitting it unsubs everything.
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:" + this.collectionName + (recordId ? ":" + recordId : "");
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 "collection:posts:*").
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(topic: string, callback: (e: RealtimeEvent) => void): void {
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
- for (const topic of this.subscriptions.keys()) {
156
- if (this.ws?.readyState === WebSocket.OPEN) {
157
- this.joinTopic(topic);
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
- if (this.token) {
235
+ const token = this.tokenProvider ? this.tokenProvider() : this.token;
236
+ if (token) {
172
237
  url +=
173
238
  (url.includes("?") ? "&" : "?") +
174
239
  "token=" +
175
- encodeURIComponent(this.token);
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(topic: string): void {
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":[]}