lazypock 0.10.2 → 0.11.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lazypock",
3
- "version": "0.10.2",
3
+ "version": "0.11.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
package/src/codegen.ts CHANGED
@@ -73,6 +73,15 @@ export function generateTypes(
73
73
  }`);
74
74
  }
75
75
 
76
+ // Resolve relation targets (collectionId → record type) for the expand maps.
77
+ // Only collections with a real id are indexed — schemas without ids (e.g.
78
+ // minimal hand-written snapshots) fall back to the name lookup below.
79
+ const collById = new Map(
80
+ filtered
81
+ .filter((c) => c.id !== undefined && c.id !== "")
82
+ .map((c) => [String(c.id), c]),
83
+ );
84
+
76
85
  // One interface per collection
77
86
  for (const coll of filtered) {
78
87
  const typeName = collectionTypeName(coll.name);
@@ -124,6 +133,42 @@ export function generateTypes(
124
133
  body: queryLines.join("\n"),
125
134
  })}`,
126
135
  );
136
+
137
+ // Expand map — relation field → target record type, so expanded
138
+ // records are typed (`record.expand.user` is `UsersRecord` instead of
139
+ // `unknown`). Built from each relation's `options.collectionId`; auth
140
+ // targets get `& AuthRecord`, multi-relations (maxSelect > 1) get
141
+ // arrays, and unknown targets (e.g. skipped collections) fall back to
142
+ // `unknown` via the `ExpandValue` fallback.
143
+ const expandLines: string[] = [];
144
+ for (const f of fields) {
145
+ if (f.type !== "relation") continue;
146
+ // Resolve the target collection: real PocketBase schemas carry
147
+ // `options.collectionId` (the target collection's id), while some
148
+ // tooling/snapshots use `options.collection` (the target's name).
149
+ // Unknown/absent targets fall back to `unknown` via ExpandValue.
150
+ // SAFETY: both option keys are strings; guard with String().
151
+ const target =
152
+ collById.get(String(f.options?.collectionId ?? "")) ??
153
+ filtered.find(
154
+ (c) => c.name === String(f.options?.collection ?? ""),
155
+ );
156
+ if (!target) continue;
157
+ const targetType = `${collectionTypeName(target.name)}Record${
158
+ target.type === "auth" ? " & AuthRecord" : ""
159
+ }`;
160
+ const many = (f.options?.maxSelect ?? 1) > 1;
161
+ expandLines.push(
162
+ ` ${JSON.stringify(fieldKey(f.name))}: ${
163
+ many ? `Array<${targetType}>` : targetType
164
+ };`,
165
+ );
166
+ }
167
+ sections.push(
168
+ `export interface ${typeName}ExpandMap${renderInterface({
169
+ body: expandLines.join("\n"),
170
+ })}`,
171
+ );
127
172
  }
128
173
 
129
174
  // Auth collection type
@@ -167,6 +212,15 @@ ${createMapEntries}
167
212
  .join("\n");
168
213
  sections.push(`export interface LazypockQueryFields {
169
214
  ${queryMapEntries}
215
+ }`);
216
+
217
+ // Expand map — relation field → target record type per collection, bound
218
+ // as the 4th CollectionService generic so expanded records are typed.
219
+ const expandMapEntries = filtered
220
+ .map((c) => ` "${c.name}": ${collectionTypeName(c.name)}ExpandMap;`)
221
+ .join("\n");
222
+ sections.push(`export interface LazypockExpandMaps {
223
+ ${expandMapEntries}
170
224
  }`);
171
225
 
172
226
  // Runtime schema snapshot — lets the client exclude hidden fields by
@@ -228,25 +282,31 @@ export class TypedClient extends LazypockClient {
228
282
  // The (string & {}) intersection keeps the union from collapsing to plain
229
283
  // string (which would silently kill the suggestions).
230
284
  //
231
- // The service binds three types: the read model, the write-only *CreateData,
232
- // and the *QueryFields (filter/sort/expand/select keys). QueryFields is what
233
- // lets hidden relation fields — excluded from the read model but still
234
- // expandable/filterable at runtime — typecheck:
285
+ // The service binds four types: the read model, the write-only *CreateData,
286
+ // the *QueryFields (filter/sort/expand/select keys), and the *ExpandMap
287
+ // (relation field → target record type). QueryFields is what lets hidden
288
+ // relation fields — excluded from the read model but still expandable/
289
+ // filterable at runtime — typecheck:
235
290
  // client.collection("project_members").getFullList({ expand: "user" }) // ✓
291
+ // ExpandMap is what types the expanded data on the result:
292
+ // const [rec] = await client.collection("posts").getFullList({ expand: "user" });
293
+ // rec.expand?.user // ✓ UsersRecord, not unknown
236
294
  override collection<K extends keyof LazypockCollections | (string & {})>(
237
295
  name: K,
238
296
  ): K extends keyof LazypockCollections
239
297
  ? CollectionService<
240
298
  LazypockCollections[K],
241
299
  LazypockCreateData[K],
242
- LazypockQueryFields[K]
300
+ LazypockQueryFields[K],
301
+ LazypockExpandMaps[K]
243
302
  >
244
303
  : CollectionService<unknown> {
245
304
  return super.collection(name) as K extends keyof LazypockCollections
246
305
  ? CollectionService<
247
306
  LazypockCollections[K],
248
307
  LazypockCreateData[K],
249
- LazypockQueryFields[K]
308
+ LazypockQueryFields[K],
309
+ LazypockExpandMaps[K]
250
310
  >
251
311
  : CollectionService<unknown>;
252
312
  }
package/src/collection.ts CHANGED
@@ -128,8 +128,17 @@ function normalizeAction(
128
128
  * Defaults to `T`. The codegen client binds a *QueryFields type that includes
129
129
  * hidden fields — excluded from the read model but still expandable/filterable
130
130
  * at runtime — so `expand: "hiddenRelation"` typechecks.
131
+ * @typeParam TExpand — Map of relation field → target record type. Defaults to
132
+ * `Record<string, unknown>`. The codegen client binds a per-collection
133
+ * *ExpandMap, so `record.expand.user` is typed as the target `UsersRecord`
134
+ * instead of `unknown`.
131
135
  */
132
- export class CollectionService<T = ApiRecord, TData = never, TFields = T> {
136
+ export class CollectionService<
137
+ T = ApiRecord,
138
+ TData = never,
139
+ TFields = T,
140
+ TExpand = Record<string, unknown>,
141
+ > {
133
142
  private http: HttpClient;
134
143
  private collectionName: string;
135
144
  private authStore?: AuthStore;
@@ -189,9 +198,9 @@ export class CollectionService<T = ApiRecord, TData = never, TFields = T> {
189
198
  */
190
199
  select<K extends FieldKey<TFields> | "*">(
191
200
  ...fields: K[]
192
- ): CollectionService<T, TData, TFields> {
201
+ ): CollectionService<T, TData, TFields, TExpand> {
193
202
  const preset = fields.length === 0 ? "*" : fields.join(",");
194
- const derived = new CollectionService<T, TData, TFields>(
203
+ const derived = new CollectionService<T, TData, TFields, TExpand>(
195
204
  this.http,
196
205
  this.collectionName,
197
206
  this.authStore,
@@ -302,7 +311,7 @@ export class CollectionService<T = ApiRecord, TData = never, TFields = T> {
302
311
  page = 1,
303
312
  perPage = 30,
304
313
  options?: ListOptions<TFields, E, S, F> & RequestOptions,
305
- ): Promise<ListResult<T2 & { expand?: ExpandObj<E> }> | null> {
314
+ ): Promise<ListResult<T2 & { expand?: ExpandObj<E, TExpand> }> | null> {
306
315
  const {
307
316
  requestKey,
308
317
  autoCancel,
@@ -332,7 +341,7 @@ export class CollectionService<T = ApiRecord, TData = never, TFields = T> {
332
341
  }).map(([k, v]) => [k, String(v)]),
333
342
  ),
334
343
  ).toString();
335
- return this.http.get<ListResult<T2 & { expand?: ExpandObj<E> }>>(
344
+ return this.http.get<ListResult<T2 & { expand?: ExpandObj<E, TExpand> }>>(
336
345
  "/" + this.encodeId(this.collectionName) + "?" + qs,
337
346
  {
338
347
  requestKey,
@@ -360,7 +369,7 @@ export class CollectionService<T = ApiRecord, TData = never, TFields = T> {
360
369
  F extends string = never,
361
370
  >(
362
371
  options?: ListOptions<TFields, E, S, F> & RequestOptions,
363
- ): Promise<Array<T2 & { expand?: ExpandObj<E> }>> {
372
+ ): Promise<Array<T2 & { expand?: ExpandObj<E, TExpand> }>> {
364
373
  const { batch = 1000, ...rest } = options ?? {};
365
374
 
366
375
  // Build a stable request key for the whole full-list fetch (NOT per page,
@@ -373,7 +382,7 @@ export class CollectionService<T = ApiRecord, TData = never, TFields = T> {
373
382
  ? rest.requestKey
374
383
  : `getFullList:${this.collectionName}:${stableStringify(rest)}`;
375
384
 
376
- const items: Array<T2 & { expand?: ExpandObj<E> }> = [];
385
+ const items: Array<T2 & { expand?: ExpandObj<E, TExpand> }> = [];
377
386
  let page = 1;
378
387
  for (;;) {
379
388
  const res = await this.getList<T2>(
@@ -386,7 +395,7 @@ export class CollectionService<T = ApiRecord, TData = never, TFields = T> {
386
395
  } as ListOptions<TFields> & RequestOptions,
387
396
  );
388
397
  if (!res || !res.items || res.items.length === 0) break;
389
- items.push(...(res.items as Array<T2 & { expand?: ExpandObj<E> }>));
398
+ items.push(...(res.items as Array<T2 & { expand?: ExpandObj<E, TExpand> }>));
390
399
  if (page >= (res.totalPages ?? page)) break;
391
400
  page += 1;
392
401
  }
@@ -407,7 +416,7 @@ export class CollectionService<T = ApiRecord, TData = never, TFields = T> {
407
416
  >(
408
417
  filter: FilterString<TFields, F>,
409
418
  options?: ListOptions<TFields, E, S, F> & RequestOptions,
410
- ): Promise<(T2 & { expand?: ExpandObj<E> }) | null> {
419
+ ): Promise<(T2 & { expand?: ExpandObj<E, TExpand> }) | null> {
411
420
  const res = await this.getList<T2, E, S, F>(1, 1, {
412
421
  ...options,
413
422
  filter,
@@ -423,7 +432,7 @@ export class CollectionService<T = ApiRecord, TData = never, TFields = T> {
423
432
  getOne<E extends string = never>(
424
433
  id: string,
425
434
  options?: ReadOptions<TFields, E> & RequestOptions,
426
- ): Promise<(T & { expand?: ExpandObj<E> }) | null> {
435
+ ): Promise<(T & { expand?: ExpandObj<E, TExpand> }) | null> {
427
436
  const fields = this.effectiveFields(options?.fields);
428
437
  if (typeof options?.expand === "string") {
429
438
  this.validateExpand(options.expand);
@@ -432,7 +441,7 @@ export class CollectionService<T = ApiRecord, TData = never, TFields = T> {
432
441
  if (fields !== undefined) qs.set("fields", fields);
433
442
  if (typeof options?.expand === "string") qs.set("expand", options.expand);
434
443
  const qsStr = qs.toString();
435
- return this.http.get<T & { expand?: ExpandObj<E> }>(
444
+ return this.http.get<T & { expand?: ExpandObj<E, TExpand> }>(
436
445
  "/" +
437
446
  this.encodeId(this.collectionName) +
438
447
  "/" +
@@ -550,8 +559,8 @@ export class CollectionService<T = ApiRecord, TData = never, TFields = T> {
550
559
  * `types.schemas`). Enables schema-aware defaults: hidden fields are
551
560
  * excluded from responses, select/expand are validated.
552
561
  */
553
- withSchema(schema: CollectionSchema): CollectionService<T, TData, TFields> {
554
- const derived = new CollectionService<T, TData, TFields>(
562
+ withSchema(schema: CollectionSchema): CollectionService<T, TData, TFields, TExpand> {
563
+ const derived = new CollectionService<T, TData, TFields, TExpand>(
555
564
  this.http,
556
565
  this.collectionName,
557
566
  this.authStore,
package/src/types.ts CHANGED
@@ -412,17 +412,27 @@ type TopExpandKey<S extends string> = S extends `${infer A}.${string}` ? A : S;
412
412
  type ExpandKeys<S extends string> = S extends "" ? never :
413
413
  S extends `${infer H},${infer T}` ? TopExpandKey<H> | ExpandKeys<T> : TopExpandKey<S>;
414
414
 
415
+ /**
416
+ * Value type of one expanded relation: the target record from the expand
417
+ * map (`TExpand`, e.g. the codegen-generated `PostsExpandMap`) when the key
418
+ * is a known relation, `unknown` otherwise.
419
+ */
420
+ type ExpandValue<K extends string, TExpand> = K extends keyof TExpand ? TExpand[K] : unknown;
421
+
415
422
  /**
416
423
  * The `expand` property attached to records when `expand` is used. Keys are
417
424
  * parsed from the caller's expand literal (dot-paths collapse to their first
418
- * segment), so `record.expand.user` typechecks and the nested record is
419
- * `unknown` (its shape belongs to the target collection). Falls back to
420
- * `Record<string, unknown>` when the expand value is dynamic or absent.
425
+ * segment) and each value is typed from `TExpand` — a per-collection map of
426
+ * relation field → target record type (the codegen CLI emits these), so
427
+ * `record.expand.user` is `UsersRecord`, not `unknown`.
428
+ *
429
+ * Falls back to `Record<string, unknown>` when the expand value is dynamic
430
+ * or absent, or when no expand map is bound (hand-written services).
421
431
  */
422
- export type ExpandObj<E extends string> =
432
+ export type ExpandObj<E extends string, TExpand = Record<string, unknown>> =
423
433
  [E] extends [never] ? Record<string, unknown> :
424
434
  [string] extends [E] ? Record<string, unknown> :
425
- Partial<Record<ExpandKeys<E>, unknown>>;
435
+ Partial<{ [K in ExpandKeys<E>]: ExpandValue<K, TExpand> }>;
426
436
 
427
437
  /**
428
438
  * Query options for list/read operations, typed against a record shape `T`.
@@ -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: 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\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\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// 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 three types: the read model, the write-only *CreateData,\n // and the *QueryFields (filter/sort/expand/select keys). QueryFields is what\n // lets hidden relation fields — excluded from the read model but still\n // expandable/filterable at runtime — typecheck:\n // client.collection(\"project_members\").getFullList({ expand: \"user\" }) // ✓\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 >\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 >\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 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":[]}