lazypock 0.4.0 → 0.6.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 CHANGED
@@ -138,6 +138,61 @@ const client = new LazypockClient({
138
138
  const code = client.generateTypes(); // string — write to lazypock.types.ts
139
139
  ```
140
140
 
141
+ > The codegen CLI emits a `lazypockSchema` snapshot next to the types, and the
142
+ > generated `createClient()` wires it in automatically — so the schema-driven
143
+ > behaviour below (hidden-field exclusion, query validation) works out of the box.
144
+
145
+ ### Field projection (`select`) & query suggestions
146
+
147
+ #### `.select(...)` — pick the fields you want
148
+
149
+ `select()` projects list/read responses to the given fields (PocketBase `fields`
150
+ param). Field names are **type-checked** when the service is typed:
151
+
152
+ ```typescript
153
+ const t = await client.collection('posts').select('id', 'title').getList();
154
+ // GET /api/posts?fields=id,title
155
+
156
+ await client.collection('posts').select('id', 'title').getOne('abc123'); // same
157
+ ```
158
+
159
+ - `select('*')` (or no `select()` call) — request **all visible fields**; hidden
160
+ fields are excluded automatically when a schema is available.
161
+ - `select()` with no arguments resets back to the default.
162
+ - `select()` returns a **derived service** — the original is untouched, so you
163
+ can keep one default service and project per-request.
164
+ - Passing an explicit `fields` option overrides the `select()` preset.
165
+
166
+ When a schema is known (via `types.schemas` or codegen), hidden fields are
167
+ **not returned by the server**: every read sends `fields=<visible fields>` by
168
+ default, and selecting an unknown field logs a warning.
169
+
170
+ #### `filter` / `sort` / `expand` — type-checked suggestions
171
+
172
+ With a typed service, the query options validate field names (and filter
173
+ operators) at compile time — your editor suggests valid fields as you type:
174
+
175
+ ```typescript
176
+ await postsSvc.getList(1, 20, { sort: '-title' }); // ✓ suggests title/published/…
177
+ await postsSvc.getList(1, 20, { sort: '-nope' }); // ✗ compile error
178
+
179
+ await postsSvc.getList(1, 20, {
180
+ filter: "title ~ 'x' && published = true", // ✓ field + operator checked
181
+ });
182
+ await postsSvc.getList(1, 20, { filter: 'nope = 1' }); // ✗ compile error
183
+
184
+ await postsSvc.getList(1, 20, { expand: 'author' }); // ✓ field suggested
185
+ await postsSvc.getOne('abc', { expand: 'author' });
186
+ ```
187
+
188
+ - `filter` — `field op value` clauses with `= != ~ !~ > >= < <=` operators;
189
+ `&&`, `||`, `!`, and parentheses are allowed after the first clause.
190
+ - `sort` — `field`, `-field` (desc), `+field`, or comma-separated.
191
+ - `expand` — comma-separated relation field names; non-relation fields warn at
192
+ runtime when a schema is available.
193
+ - The **untyped** client (`client.collection('posts')` without `typed<T>()`)
194
+ still accepts any string — suggestions kick in once the service is typed.
195
+
141
196
  ### CLI reference
142
197
 
143
198
  ```bash
@@ -179,7 +234,6 @@ The main client class.
179
234
  | `storage` | `StorageAdapter` | `memoryStorage` | Custom storage adapter for token persistence |
180
235
  | `authStore` | `AuthStore` | auto-created | Explicit auth store instance |
181
236
  | `realtime` | `RealtimeService` | auto-created | Real-time service for WebSocket subscriptions |
182
- | `cache` | [`CacheConfig`](#query-cache) | disabled | Query-cache configuration (opt-in) |
183
237
 
184
238
  #### Auto-Cancellation Methods
185
239
 
@@ -187,14 +241,6 @@ The main client class.
187
241
  - `cancelRequest(requestKey)` — Abort a single pending request by key (default `HTTP_METHOD + path`)
188
242
  - `cancelAllRequests()` — Abort all pending requests
189
243
 
190
- #### Query-Cache Methods
191
-
192
- - `cache(config?)` — Enable/configure the query cache at runtime (see [Query Cache](#query-cache))
193
- - `clearCache()` — Drop every cached entry
194
- - `invalidateCache(namespace)` — Invalidate entries for a collection / custom namespace
195
- - `cacheStats()` — `{ hits, misses, entries }` cache statistics
196
- - `invalidateCacheOnRealtime(collection)` — Subscribe the cache to realtime events for a collection; returns an unsubscribe fn
197
-
198
244
  #### Authentication Methods
199
245
 
200
246
  - `login(email, password, collection?)` — Login as superuser or auth collection user
@@ -237,7 +283,8 @@ PocketBase-style service for the collections themselves (admin):
237
283
 
238
284
  Returned by `client.collection(name)`.
239
285
 
240
- - `getList(page, perPage, options?)` Paginated list of records
286
+ - `select(...fields)` Project reads to the given fields (see [Field projection](#field-projection-select--query-suggestions)); `select('*')` restores the all-visible default
287
+ - `getList(page, perPage, options?)` — Paginated list of records (typed `filter`/`sort`/`expand`/`fields`)
241
288
  - `getFullList(options?)` — Fetch all records (auto-paginates)
242
289
  - `getFirstListItem(filter, options?)` — Fetch first record matching filter
243
290
  - `getOne(id, options?)` — Get record by ID
@@ -386,104 +433,6 @@ want to coalesce concurrent identical calls yourself:
386
433
  await client.collection('posts').getList(1, 20, { singleFlight: true });
387
434
  ```
388
435
 
389
- ## Query Cache
390
-
391
- Lazypock has a built-in query cache for **GET** requests — disabled by default.
392
- It's useful for read-heavy UIs (lists, dashboards) to avoid hammering the server.
393
-
394
- ### Enabling
395
-
396
- ```typescript
397
- import { createClient } from "lazypock";
398
-
399
- const client = createClient({
400
- baseUrl: "https://api.example.com",
401
- cache: {
402
- enabled: true,
403
- defaultTTL: 30_000, // 30s
404
- // store: myStorage, // optional: reuse any StorageAdapter (localStorage/AsyncStorage)
405
- },
406
- });
407
- ```
408
-
409
- When enabled, **all** GET requests are cached with the default TTL, and
410
- mutations (`create`/`update`/`delete`) automatically invalidate the affected
411
- collection's cached entries.
412
-
413
- ### Per-request control
414
-
415
- ```typescript
416
- // Cache this request (works even when the global cache is off)
417
- await client.collection('posts').getList(1, 20, { cache: true });
418
-
419
- // Bypass the cache — always fetch fresh (and don't store the result)
420
- const fresh = await client.collection('posts').getList(1, 20, { cache: false });
421
-
422
- // Custom TTL for this request
423
- await client.collection('posts').getOne('abc', { ttl: 120_000 });
424
-
425
- // Cache with a custom key (dedupe/override the default `METHOD path|token` key)
426
- await client.collection('posts').getList(1, 20, { cache: { ttl: 60_000, key: 'my-list' } });
427
- ```
428
-
429
- ### Prefix deletion (per-operation invalidation)
430
-
431
- Every cached entry is tagged with its operation and collection, so you can
432
- invalidate a whole class of caches without touching the rest:
433
-
434
- ```typescript
435
- client.cache.deleteByPrefix('getList:posts'); // delete all getList cache for posts
436
- client.cache.deleteByPrefix('getOne:posts'); // delete all getOne cache for posts
437
- client.cache.deleteByPrefix('collections:getList'); // admin collection list caches
438
- ```
439
-
440
- The `client.cache` namespace also exposes `invalidate(ns)`, `clear()`, `stats()`,
441
- and is callable to (re)configure (`client.cache({ enabled: true })`).
442
-
443
- ### Invalidation
444
-
445
- ```typescript
446
- // Mutations invalidate the collection automatically:
447
- await client.collection('posts').create({ title: 'New' });
448
- await client.collection('posts').getList(1, 20); // re-fetched (cache cleared)
449
-
450
- // Invalidate extra namespaces explicitly:
451
- await client.collection('posts').create(
452
- { title: 'New' },
453
- { invalidate: ['users'] },
454
- );
455
-
456
- // Manual / out-of-band invalidation:
457
- client.invalidateCache('posts');
458
- client.clearCache();
459
- ```
460
-
461
- ### Cache key scoping
462
-
463
- Cache keys are **scoped by auth token** — a logged-in user's cached data can
464
- never leak to another user (or to anonymous visitors). Logging out/in changes
465
- the token, so cached entries are naturally isolated per identity.
466
-
467
- ### Realtime-driven invalidation
468
-
469
- ```typescript
470
- // Keep the posts cache fresh: any create/update/delete event clears it.
471
- const stop = client.invalidateCacheOnRealtime('posts');
472
- // later:
473
- stop();
474
- ```
475
-
476
- When a realtime event arrives for the collection, its cached entries are
477
- cleared so the next read fetches fresh data. This is **invalidate-only** —
478
- cached list payloads are never mutated in place (a filter/sort change could
479
- make an in-place patch serve wrong data).
480
-
481
- ### Stats
482
-
483
- ```typescript
484
- client.cacheStats(); // { hits, misses, entries }
485
- ```
486
-
487
436
  ## Error Handling
488
437
 
489
438
  The SDK throws `ApiError` on non-2xx responses:
@@ -146,15 +146,39 @@ function generateTypes(collections, options = {}) {
146
146
  sections.push(`export interface LazypockCollections {
147
147
  ${mapEntries}
148
148
  }`);
149
+ const schemaEntries = collections.map(
150
+ (c) => ` {
151
+ id: ${JSON.stringify(c.id ?? null)},
152
+ name: ${JSON.stringify(c.name)},
153
+ type: ${JSON.stringify(c.type)},
154
+ system: ${JSON.stringify(c.system ?? false)},
155
+ fields: ${JSON.stringify(c.fields ?? [], null, 2)},
156
+ rules: ${JSON.stringify(c.rules ?? {})},
157
+ options: ${JSON.stringify(c.options ?? {})},
158
+ }`
159
+ ).join(",\n");
160
+ sections.push(`// Schema snapshot (runtime) \u2014 hidden-field exclusion + query validation.
161
+ export const lazypockSchema: import("${packageName}").CollectionSchema[] = [
162
+ ${schemaEntries}
163
+ ];`);
149
164
  sections.push(`import { LazypockClient, type LazypockClientOptions, type CollectionService } from "${packageName}";
150
165
 
151
166
  /**
152
167
  * Create a Lazypock client typed against this schema snapshot.
153
168
  * Collection access is fully type-checked:
154
169
  * client.collection("posts").create({ title: "x" }) // title must exist
170
+ *
171
+ * The schema snapshot is wired into the client automatically, so hidden
172
+ * fields are excluded from responses and select/expand are validated.
155
173
  */
156
174
  export function createClient(options: LazypockClientOptions): TypedClient {
157
- return new TypedClient(options);
175
+ return new TypedClient({
176
+ ...options,
177
+ types: {
178
+ ...options.types,
179
+ schemas: options.types?.schemas ?? lazypockSchema,
180
+ },
181
+ });
158
182
  }
159
183
 
160
184
  export class TypedClient extends LazypockClient {
@@ -175,6 +199,7 @@ ${opts.body}
175
199
  }`;
176
200
  }
177
201
  function memberLine(f) {
202
+ if (f.hidden) return "";
178
203
  const key = fieldKey(f.name);
179
204
  const req = f.required || f.type === "password" ? "" : "?";
180
205
  const type = fieldTypeScriptType(f);
@@ -189,4 +214,4 @@ export {
189
214
  collectionTypeName,
190
215
  generateTypes
191
216
  };
192
- //# sourceMappingURL=chunk-Y6PMBL2S.js.map
217
+ //# sourceMappingURL=chunk-HOBNUW5Z.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/typegen.ts","../src/codegen.ts"],"sourcesContent":["// ── Type mapping (runtime + codegen) ────────────────────\n// Maps server field types to TypeScript types.\n// Shared by the runtime `SchemaTypes` helper and the codegen CLI.\n\nimport type { SchemaField } from \"./schema\";\n\n/**\n * Map a single server field to its TypeScript type string.\n * Used by the codegen CLI to emit interface members.\n *\n * @param field The field definition.\n * @param fallback Fallback type for unknown field types (default `unknown`).\n */\nexport function fieldTypeScriptType(\n\tfield: SchemaField,\n\tfallback = \"unknown\",\n): string {\n\tconst opts = field.options ?? {};\n\tswitch (field.type) {\n\t\tcase \"text\":\n\t\tcase \"email\":\n\t\tcase \"url\":\n\t\tcase \"editor\":\n\t\tcase \"date\":\n\t\tcase \"datetime\":\n\t\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\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// 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 */\nexport function createClient(options: LazypockClientOptions): TypedClient {\n return new TypedClient(options);\n}\n\nexport class TypedClient extends LazypockClient {\n override collection<T extends string>(name: T): T extends keyof LazypockCollections\n ? CollectionService<LazypockCollections[T]>\n : CollectionService<unknown> {\n return super.collection(name) as T extends keyof LazypockCollections ? CollectionService<LazypockCollections[T]> : 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\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"],"mappings":";AAaO,SAAS,oBACf,OACA,WAAW,WACF;AACT,QAAM,OAAO,MAAM,WAAW,CAAC;AAC/B,UAAQ,MAAM,MAAM;AAAA,IACnB,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AACJ,aAAO;AAAA,IACR,KAAK;AACJ,aAAO;AAAA,IACR,KAAK;AACJ,aAAO;AAAA,IACR,KAAK,UAAU;AACd,YAAM,SAAS,MAAM,QAAQ,KAAK,MAAM,IAAI,KAAK,SAAS,CAAC;AAC3D,UAAI,OAAO,SAAS,GAAG;AACtB,eAAO,OAAO,IAAI,CAAC,MAAM,KAAK,UAAU,OAAO,CAAC,CAAC,CAAC,EAAE,KAAK,KAAK;AAAA,MAC/D;AACA,aAAO;AAAA,IACR;AAAA,IACA,KAAK,gBAAgB;AACpB,YAAM,SAAS,MAAM,QAAQ,KAAK,MAAM,IAAI,KAAK,SAAS,CAAC;AAC3D,UAAI,OAAO,SAAS,GAAG;AACtB,eAAO,IAAI,OAAO,IAAI,CAAC,MAAM,KAAK,UAAU,OAAO,CAAC,CAAC,CAAC,EAAE,KAAK,KAAK,CAAC;AAAA,MACpE;AACA,aAAO;AAAA,IACR;AAAA,IACA,KAAK;AACJ,aAAO;AAAA,IACR,KAAK;AACJ,aAAO;AAAA,IACR,KAAK;AAAA,IACL,KAAK;AACJ,aAAO;AAAA,IACR,KAAK;AAGJ,cAAQ,KAAK,aAAa,KAAK,IAAI,aAAa;AAAA,IACjD,KAAK;AAEJ,aAAO;AAAA,IACR;AACC,aAAO;AAAA,EACT;AACD;AAkBO,SAAS,cAAc,OAAmC;AAChE,QAAM,OAAO,MAAM,WAAW,CAAC;AAC/B,UAAQ,MAAM,MAAM;AAAA,IACnB,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AACJ,aAAO;AAAA,IACR,KAAK;AACJ,aAAO;AAAA,IACR,KAAK;AACJ,aAAO;AAAA,IACR,KAAK;AAAA,IACL,KAAK;AACJ,aAAO;AAAA,IACR,KAAK;AAAA,IACL,KAAK;AACJ,aAAO;AAAA,IACR,KAAK;AACJ,cAAQ,KAAK,aAAa,KAAK,IAAI,kBAAkB;AAAA,IACtD,KAAK;AACJ,aAAO;AAAA,IACR;AACC,aAAO;AAAA,EACT;AACD;AAOO,SAAS,gBAAgB,OAA6B;AAC5D,UAAQ,cAAc,KAAK,GAAG;AAAA,IAC7B,KAAK;AACJ,aAAO;AAAA,IACR,KAAK;AACJ,aAAO;AAAA,IACR,KAAK;AACJ,aAAO;AAAA,IACR,KAAK;AACJ,aAAO,CAAC,MAAM;AAAA,IACf,KAAK;AACJ,aAAO;AAAA,IACR,KAAK;AACJ,aAAO,CAAC,MAAM;AAAA,IACf,KAAK;AACJ,aAAO;AAAA,IACR,KAAK;AACJ,aAAO;AAAA,IACR,KAAK;AACJ,aAAO;AAAA,EACT;AACD;;;ACzHO,SAAS,mBAAmB,MAAsB;AACxD,SAAO,KACL,MAAM,eAAe,EACrB,OAAO,OAAO,EACd,IAAI,CAAC,SAAS,KAAK,OAAO,CAAC,EAAE,YAAY,IAAI,KAAK,MAAM,CAAC,CAAC,EAC1D,KAAK,EAAE,EACP,QAAQ,UAAU,KAAK;AAC1B;AAGO,SAAS,SAAS,MAAsB;AAC9C,SAAO,KAAK,QAAQ,kBAAkB,GAAG;AAC1C;AAUO,SAAS,cACf,aACA,UAOI,CAAC,GACI;AACT,QAAM;AAAA,IACL,cAAc;AAAA,IACd,oBAAoB;AAAA,IACpB,aAAa;AAAA,EACd,IAAI;AAEJ,QAAM,WAAW,aACd,YAAY;AAAA,IACZ,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC,EAAE,KAAK,WAAW,GAAG,KAAK,EAAE,SAAS;AAAA,EAC3D,IACC;AAEH,QAAM,WAAqB,CAAC;AAC5B,WAAS,KAAK;AAAA;AAAA,uBAEO,oBAAI,KAAK,GAAE,YAAY,CAAC,EAAE;AAE/C,MAAI,mBAAmB;AACtB,aAAS,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMd;AAAA,EACD;AAGA,aAAW,QAAQ,UAAU;AAC5B,UAAM,WAAW,mBAAmB,KAAK,IAAI;AAC7C,UAAM,SAAS,KAAK,UAAU,CAAC;AAC/B,UAAM,QAAQ,OAAO,IAAI,CAAC,MAAM,WAAW,CAAC,CAAC,EAAE,OAAO,CAAC,MAAM,MAAM,EAAE;AACrE,UAAM,OAAO,MAAM,KAAK,IAAI;AAC5B,aAAS;AAAA,MACR,oBAAoB,QAAQ,SAAS,gBAAgB;AAAA,QACpD,SAAS,oBAAoB,eAAe;AAAA,QAC5C;AAAA,MACD,CAAC,CAAC;AAAA,IACH;AAAA,EACD;AAGA,WAAS,KAAK;AAAA;AAAA;AAAA,EAGb;AAGD,QAAM,aAAa,SACjB;AAAA,IACA,CAAC,MACA,MAAM,EAAE,IAAI,MAAM,mBAAmB,EAAE,IAAI,CAAC,SAC3C,EAAE,SAAS,SAAS,kBAAkB,EACvC;AAAA,EACF,EACC,KAAK,IAAI;AACX,WAAS,KAAK;AAAA,EACb,UAAU;AAAA,EACV;AAGD,WAAS,KAAK,uFAAuF,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAkBhH;AAEA,SAAO,SAAS,KAAK,MAAM,IAAI;AAChC;AAMA,SAAS,gBAAgB,MAAkD;AAC1E,QAAM,MAAM,KAAK,UAAU,YAAY,KAAK,OAAO,KAAK;AACxD,MAAI,CAAC,KAAK,KAAM,QAAO,GAAG,GAAG;AAC7B,SAAO,GAAG,GAAG;AAAA,EAAO,KAAK,IAAI;AAAA;AAC9B;AAGA,SAAS,WAAW,GAAwB;AAC3C,QAAM,MAAM,SAAS,EAAE,IAAI;AAC3B,QAAM,MAAM,EAAE,YAAY,EAAE,SAAS,aAAa,KAAK;AACvD,QAAM,OAAO,oBAAoB,CAAC;AAElC,MAAI,SAAS,QAAS,QAAO;AAC7B,SAAO,KAAK,KAAK,UAAU,GAAG,CAAC,GAAG,GAAG,KAAK,IAAI;AAC/C;","names":[]}
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\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// 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 * 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 override collection<T extends string>(name: T): T extends keyof LazypockCollections\n ? CollectionService<LazypockCollections[T]>\n : CollectionService<unknown> {\n return super.collection(name) as T extends keyof LazypockCollections ? CollectionService<LazypockCollections[T]> : 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"],"mappings":"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","names":[]}
package/dist/cli.cjs CHANGED
@@ -101,15 +101,39 @@ function generateTypes(collections, options = {}) {
101
101
  sections.push(`export interface LazypockCollections {
102
102
  ${mapEntries}
103
103
  }`);
104
+ const schemaEntries = collections.map(
105
+ (c) => ` {
106
+ id: ${JSON.stringify(c.id ?? null)},
107
+ name: ${JSON.stringify(c.name)},
108
+ type: ${JSON.stringify(c.type)},
109
+ system: ${JSON.stringify(c.system ?? false)},
110
+ fields: ${JSON.stringify(c.fields ?? [], null, 2)},
111
+ rules: ${JSON.stringify(c.rules ?? {})},
112
+ options: ${JSON.stringify(c.options ?? {})},
113
+ }`
114
+ ).join(",\n");
115
+ sections.push(`// Schema snapshot (runtime) \u2014 hidden-field exclusion + query validation.
116
+ export const lazypockSchema: import("${packageName}").CollectionSchema[] = [
117
+ ${schemaEntries}
118
+ ];`);
104
119
  sections.push(`import { LazypockClient, type LazypockClientOptions, type CollectionService } from "${packageName}";
105
120
 
106
121
  /**
107
122
  * Create a Lazypock client typed against this schema snapshot.
108
123
  * Collection access is fully type-checked:
109
124
  * client.collection("posts").create({ title: "x" }) // title must exist
125
+ *
126
+ * The schema snapshot is wired into the client automatically, so hidden
127
+ * fields are excluded from responses and select/expand are validated.
110
128
  */
111
129
  export function createClient(options: LazypockClientOptions): TypedClient {
112
- return new TypedClient(options);
130
+ return new TypedClient({
131
+ ...options,
132
+ types: {
133
+ ...options.types,
134
+ schemas: options.types?.schemas ?? lazypockSchema,
135
+ },
136
+ });
113
137
  }
114
138
 
115
139
  export class TypedClient extends LazypockClient {
@@ -130,6 +154,7 @@ ${opts.body}
130
154
  }`;
131
155
  }
132
156
  function memberLine(f) {
157
+ if (f.hidden) return "";
133
158
  const key = fieldKey(f.name);
134
159
  const req = f.required || f.type === "password" ? "" : "?";
135
160
  const type = fieldTypeScriptType(f);
package/dist/cli.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cli.ts","../src/typegen.ts","../src/codegen.ts"],"sourcesContent":["#!/usr/bin/env node\n// ── Codegen CLI ─────────────────────────────────────────\n// `lazypock` — fetches the live collection schema from a Lazypock\n// API and writes a fully-typed `lazypock.types.ts` module.\n//\n// Auth methods (pick one):\n// 1. Superuser email + password:\n// npx lazypock --url http://localhost:4000/api --email admin@... --password ...\n// 2. API key (recommended, generated from the Settings dashboard):\n// npx lazypock --url http://localhost:4000/api --apikey <key>\n//\n// Or via env vars (no flags):\n// LAZYPOCK_URL=... LAZYPOCK_API_KEY=... npx lazypock\n// LAZYPOCK_URL=... LAZYPOCK_EMAIL=... LAZYPOCK_PASSWORD=... npx lazypock\n//\n// NOTE: `lazypock-gen` remains as a deprecated alias for backwards\n// compatibility. Both invoke the same executable.\n\nimport { writeFile } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport { generateTypes } from \"./codegen\";\nimport type { CollectionsResponse } from \"./schema\";\n\ninterface CliOptions {\n\turl: string;\n\temail: string;\n\tpassword: string;\n\tapiKey: string;\n\tout: string;\n\tpackageName: string;\n\tskipSystem: boolean;\n}\n\nfunction fail(msg: string): never {\n\tconsole.error(`\\n❌ ${msg}\\n`);\n\tprocess.exit(1);\n}\n\nfunction parseArgs(argv: string[]): CliOptions {\n\tconst args = [...argv];\n\tconst get = (flag: string, envKey: string, def = \"\"): string => {\n\t\tconst i = args.indexOf(flag);\n\t\tif (i !== -1 && i + 1 < args.length) return args[i + 1];\n\t\treturn process.env[envKey] ?? def;\n\t};\n\tconst has = (flag: string): boolean => args.includes(flag);\n\n\tconst url = get(\"--url\", \"LAZYPOCK_URL\");\n\tconst email = get(\"--email\", \"LAZYPOCK_EMAIL\");\n\tconst password = get(\"--password\", \"LAZYPOCK_PASSWORD\");\n\tconst apiKey =\n\t\tget(\"--apikey\", \"LAZYPOCK_API_KEY\") || get(\"--api-key\", \"LAZYPOCK_API_KEY\");\n\n\tif (!url) fail(\"Missing API URL. Pass --url or set LAZYPOCK_URL.\");\n\tif (!apiKey) {\n\t\tif (!email)\n\t\t\tfail(\n\t\t\t\t\"Missing credentials. Pass --apikey, or --email + --password, or set LAZYPOCK_API_KEY / LAZYPOCK_EMAIL.\",\n\t\t\t);\n\t\tif (!password)\n\t\t\tfail(\"Missing password. Pass --password, or set LAZYPOCK_PASSWORD.\");\n\t}\n\n\tconst out =\n\t\tget(\"--output\", \"LAZYPOCK_OUT\") ||\n\t\tget(\"--out\", \"LAZYPOCK_OUT\", \"lazypock.types.ts\");\n\tconst packageName = get(\"--package\", \"LAZYPOCK_PACKAGE\", \"lazypock\");\n\n\treturn {\n\t\turl,\n\t\temail,\n\t\tpassword,\n\t\tapiKey,\n\t\tout,\n\t\tpackageName,\n\t\tskipSystem: has(\"--skip-system\"),\n\t};\n}\n\nasync function fetchCollections(\n\topts: CliOptions,\n): Promise<CollectionsResponse> {\n\tconst base = opts.url.replace(/\\/+$/, \"\");\n\n\tlet authKey: string;\n\tif (opts.apiKey) {\n\t\t// Step 1: use a stored API key directly (no login round-trip).\n\t\t// The key is sent as `Authorization: Bearer <key>` and recognised\n\t\t// by the backend's Auth.Plug as an API key.\n\t\tauthKey = opts.apiKey;\n\t} else {\n\t\t// Step 1: login as superuser to get a token.\n\t\t// Prefer the PocketBase-parity `_superusers` auth collection endpoint,\n\t\t// fall back to the legacy /superusers/login for older servers.\n\t\tlet loginRes = await fetch(base + \"/_superusers/auth-with-password\", {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: { \"Content-Type\": \"application/json\" },\n\t\t\tbody: JSON.stringify({ identity: opts.email, password: opts.password }),\n\t\t});\n\t\tif (!loginRes.ok) {\n\t\t\tloginRes = await fetch(base + \"/superusers/login\", {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: { \"Content-Type\": \"application/json\" },\n\t\t\t\tbody: JSON.stringify({ email: opts.email, password: opts.password }),\n\t\t\t});\n\t\t}\n\t\tif (!loginRes.ok) {\n\t\t\tconst text = await loginRes.text();\n\t\t\tfail(\n\t\t\t\t`Superuser login failed (${loginRes.status}). ${text.slice(0, 200)}`,\n\t\t\t);\n\t\t}\n\t\tconst loginData = (await loginRes.json()) as { token?: string };\n\t\tif (!loginData.token) fail(\"Login response did not include a token.\");\n\t\tauthKey = loginData.token;\n\t}\n\n\t// Step 2: fetch collections\n\tconst collRes = await fetch(base + \"/collections\", {\n\t\theaders: { Authorization: \"Bearer \" + authKey },\n\t});\n\tif (!collRes.ok) {\n\t\tconst text = await collRes.text();\n\t\tfail(\n\t\t\t`Failed to fetch collections (${collRes.status}). ${text.slice(0, 200)}`,\n\t\t);\n\t}\n\treturn (await collRes.json()) as CollectionsResponse;\n}\n\nasync function main(): Promise<void> {\n\tconst opts = parseArgs(process.argv.slice(2));\n\tconst authLabel = opts.apiKey\n\t\t? `API key ${opts.apiKey.slice(0, 4)}…${opts.apiKey.slice(-4)}`\n\t\t: opts.email;\n\tconsole.log(`\\n🔌 Connecting to ${opts.url} as ${authLabel} …`);\n\n\tconst { items } = await fetchCollections(opts);\n\tconsole.log(`📦 Found ${items.length} collection(s).`);\n\n\tconst source = generateTypes(items, {\n\t\tpackageName: opts.packageName,\n\t\tskipSystem: opts.skipSystem,\n\t});\n\n\tconst outPath = resolve(process.cwd(), opts.out);\n\tawait writeFile(outPath, source, \"utf8\");\n\tconsole.log(`✅ Wrote ${outPath} (${source.length} bytes).`);\n\tconsole.log(\n\t\t`\\nImport it in your app:\\n import { createClient } from './${opts.out.replace(/\\.ts$/, \"\")}';\\n`,\n\t);\n}\n\nmain().catch((err) => {\n\tconsole.error(err);\n\tprocess.exit(1);\n});\n","// ── 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\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// 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 */\nexport function createClient(options: LazypockClientOptions): TypedClient {\n return new TypedClient(options);\n}\n\nexport class TypedClient extends LazypockClient {\n override collection<T extends string>(name: T): T extends keyof LazypockCollections\n ? CollectionService<LazypockCollections[T]>\n : CollectionService<unknown> {\n return super.collection(name) as T extends keyof LazypockCollections ? CollectionService<LazypockCollections[T]> : 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\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"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/cli.ts","../src/typegen.ts","../src/codegen.ts"],"sourcesContent":["#!/usr/bin/env node\n// ── Codegen CLI ─────────────────────────────────────────\n// `lazypock` — fetches the live collection schema from a Lazypock\n// API and writes a fully-typed `lazypock.types.ts` module.\n//\n// Auth methods (pick one):\n// 1. Superuser email + password:\n// npx lazypock --url http://localhost:4000/api --email admin@... --password ...\n// 2. API key (recommended, generated from the Settings dashboard):\n// npx lazypock --url http://localhost:4000/api --apikey <key>\n//\n// Or via env vars (no flags):\n// LAZYPOCK_URL=... LAZYPOCK_API_KEY=... npx lazypock\n// LAZYPOCK_URL=... LAZYPOCK_EMAIL=... LAZYPOCK_PASSWORD=... npx lazypock\n//\n// NOTE: `lazypock-gen` remains as a deprecated alias for backwards\n// compatibility. Both invoke the same executable.\n\nimport { writeFile } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport { generateTypes } from \"./codegen\";\nimport type { CollectionsResponse } from \"./schema\";\n\ninterface CliOptions {\n\turl: string;\n\temail: string;\n\tpassword: string;\n\tapiKey: string;\n\tout: string;\n\tpackageName: string;\n\tskipSystem: boolean;\n}\n\nfunction fail(msg: string): never {\n\tconsole.error(`\\n❌ ${msg}\\n`);\n\tprocess.exit(1);\n}\n\nfunction parseArgs(argv: string[]): CliOptions {\n\tconst args = [...argv];\n\tconst get = (flag: string, envKey: string, def = \"\"): string => {\n\t\tconst i = args.indexOf(flag);\n\t\tif (i !== -1 && i + 1 < args.length) return args[i + 1];\n\t\treturn process.env[envKey] ?? def;\n\t};\n\tconst has = (flag: string): boolean => args.includes(flag);\n\n\tconst url = get(\"--url\", \"LAZYPOCK_URL\");\n\tconst email = get(\"--email\", \"LAZYPOCK_EMAIL\");\n\tconst password = get(\"--password\", \"LAZYPOCK_PASSWORD\");\n\tconst apiKey =\n\t\tget(\"--apikey\", \"LAZYPOCK_API_KEY\") || get(\"--api-key\", \"LAZYPOCK_API_KEY\");\n\n\tif (!url) fail(\"Missing API URL. Pass --url or set LAZYPOCK_URL.\");\n\tif (!apiKey) {\n\t\tif (!email)\n\t\t\tfail(\n\t\t\t\t\"Missing credentials. Pass --apikey, or --email + --password, or set LAZYPOCK_API_KEY / LAZYPOCK_EMAIL.\",\n\t\t\t);\n\t\tif (!password)\n\t\t\tfail(\"Missing password. Pass --password, or set LAZYPOCK_PASSWORD.\");\n\t}\n\n\tconst out =\n\t\tget(\"--output\", \"LAZYPOCK_OUT\") ||\n\t\tget(\"--out\", \"LAZYPOCK_OUT\", \"lazypock.types.ts\");\n\tconst packageName = get(\"--package\", \"LAZYPOCK_PACKAGE\", \"lazypock\");\n\n\treturn {\n\t\turl,\n\t\temail,\n\t\tpassword,\n\t\tapiKey,\n\t\tout,\n\t\tpackageName,\n\t\tskipSystem: has(\"--skip-system\"),\n\t};\n}\n\nasync function fetchCollections(\n\topts: CliOptions,\n): Promise<CollectionsResponse> {\n\tconst base = opts.url.replace(/\\/+$/, \"\");\n\n\tlet authKey: string;\n\tif (opts.apiKey) {\n\t\t// Step 1: use a stored API key directly (no login round-trip).\n\t\t// The key is sent as `Authorization: Bearer <key>` and recognised\n\t\t// by the backend's Auth.Plug as an API key.\n\t\tauthKey = opts.apiKey;\n\t} else {\n\t\t// Step 1: login as superuser to get a token.\n\t\t// Prefer the PocketBase-parity `_superusers` auth collection endpoint,\n\t\t// fall back to the legacy /superusers/login for older servers.\n\t\tlet loginRes = await fetch(base + \"/_superusers/auth-with-password\", {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: { \"Content-Type\": \"application/json\" },\n\t\t\tbody: JSON.stringify({ identity: opts.email, password: opts.password }),\n\t\t});\n\t\tif (!loginRes.ok) {\n\t\t\tloginRes = await fetch(base + \"/superusers/login\", {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: { \"Content-Type\": \"application/json\" },\n\t\t\t\tbody: JSON.stringify({ email: opts.email, password: opts.password }),\n\t\t\t});\n\t\t}\n\t\tif (!loginRes.ok) {\n\t\t\tconst text = await loginRes.text();\n\t\t\tfail(\n\t\t\t\t`Superuser login failed (${loginRes.status}). ${text.slice(0, 200)}`,\n\t\t\t);\n\t\t}\n\t\tconst loginData = (await loginRes.json()) as { token?: string };\n\t\tif (!loginData.token) fail(\"Login response did not include a token.\");\n\t\tauthKey = loginData.token;\n\t}\n\n\t// Step 2: fetch collections\n\tconst collRes = await fetch(base + \"/collections\", {\n\t\theaders: { Authorization: \"Bearer \" + authKey },\n\t});\n\tif (!collRes.ok) {\n\t\tconst text = await collRes.text();\n\t\tfail(\n\t\t\t`Failed to fetch collections (${collRes.status}). ${text.slice(0, 200)}`,\n\t\t);\n\t}\n\treturn (await collRes.json()) as CollectionsResponse;\n}\n\nasync function main(): Promise<void> {\n\tconst opts = parseArgs(process.argv.slice(2));\n\tconst authLabel = opts.apiKey\n\t\t? `API key ${opts.apiKey.slice(0, 4)}…${opts.apiKey.slice(-4)}`\n\t\t: opts.email;\n\tconsole.log(`\\n🔌 Connecting to ${opts.url} as ${authLabel} …`);\n\n\tconst { items } = await fetchCollections(opts);\n\tconsole.log(`📦 Found ${items.length} collection(s).`);\n\n\tconst source = generateTypes(items, {\n\t\tpackageName: opts.packageName,\n\t\tskipSystem: opts.skipSystem,\n\t});\n\n\tconst outPath = resolve(process.cwd(), opts.out);\n\tawait writeFile(outPath, source, \"utf8\");\n\tconsole.log(`✅ Wrote ${outPath} (${source.length} bytes).`);\n\tconsole.log(\n\t\t`\\nImport it in your app:\\n import { createClient } from './${opts.out.replace(/\\.ts$/, \"\")}';\\n`,\n\t);\n}\n\nmain().catch((err) => {\n\tconsole.error(err);\n\tprocess.exit(1);\n});\n","// ── 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\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// 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 * 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 override collection<T extends string>(name: T): T extends keyof LazypockCollections\n ? CollectionService<LazypockCollections[T]>\n : CollectionService<unknown> {\n return super.collection(name) as T extends keyof LazypockCollections ? CollectionService<LazypockCollections[T]> : 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"],"mappings":"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","names":[]}
@@ -108,15 +108,39 @@ var Lazypock = (() => {
108
108
  sections.push(`export interface LazypockCollections {
109
109
  ${mapEntries}
110
110
  }`);
111
+ const schemaEntries = collections.map(
112
+ (c) => ` {
113
+ id: ${JSON.stringify(c.id ?? null)},
114
+ name: ${JSON.stringify(c.name)},
115
+ type: ${JSON.stringify(c.type)},
116
+ system: ${JSON.stringify(c.system ?? false)},
117
+ fields: ${JSON.stringify(c.fields ?? [], null, 2)},
118
+ rules: ${JSON.stringify(c.rules ?? {})},
119
+ options: ${JSON.stringify(c.options ?? {})},
120
+ }`
121
+ ).join(",\n");
122
+ sections.push(`// Schema snapshot (runtime) \u2014 hidden-field exclusion + query validation.
123
+ export const lazypockSchema: import("${packageName}").CollectionSchema[] = [
124
+ ${schemaEntries}
125
+ ];`);
111
126
  sections.push(`import { LazypockClient, type LazypockClientOptions, type CollectionService } from "${packageName}";
112
127
 
113
128
  /**
114
129
  * Create a Lazypock client typed against this schema snapshot.
115
130
  * Collection access is fully type-checked:
116
131
  * client.collection("posts").create({ title: "x" }) // title must exist
132
+ *
133
+ * The schema snapshot is wired into the client automatically, so hidden
134
+ * fields are excluded from responses and select/expand are validated.
117
135
  */
118
136
  export function createClient(options: LazypockClientOptions): TypedClient {
119
- return new TypedClient(options);
137
+ return new TypedClient({
138
+ ...options,
139
+ types: {
140
+ ...options.types,
141
+ schemas: options.types?.schemas ?? lazypockSchema,
142
+ },
143
+ });
120
144
  }
121
145
 
122
146
  export class TypedClient extends LazypockClient {
@@ -137,6 +161,7 @@ ${opts.body}
137
161
  }`;
138
162
  }
139
163
  function memberLine(f) {
164
+ if (f.hidden) return "";
140
165
  const key = fieldKey(f.name);
141
166
  const req = f.required || f.type === "password" ? "" : "?";
142
167
  const type = fieldTypeScriptType(f);