lazypock 0.1.0 → 0.1.2

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/dist/index.cjs CHANGED
@@ -22,10 +22,18 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  ApiError: () => ApiError,
24
24
  AuthStore: () => AuthStore,
25
+ CollectionService: () => CollectionService,
25
26
  FilesService: () => FilesService,
26
27
  LazypockClient: () => LazypockClient,
27
28
  RealtimeService: () => RealtimeService,
29
+ TypedClient: () => TypedClient,
30
+ collectionTypeName: () => collectionTypeName,
31
+ createClient: () => createClient,
32
+ fieldTypeKind: () => fieldTypeKind,
33
+ fieldTypeScriptType: () => fieldTypeScriptType,
34
+ generateTypes: () => generateTypes,
28
35
  getFileUrl: () => getFileUrl,
36
+ schemaFieldType: () => schemaFieldType,
29
37
  wsUrlFromBaseUrl: () => wsUrlFromBaseUrl
30
38
  });
31
39
  module.exports = __toCommonJS(index_exports);
@@ -321,12 +329,25 @@ var memoryStorage = {
321
329
  };
322
330
 
323
331
  // src/collection.ts
332
+ function normalizeAction(event, rawAction) {
333
+ if (typeof rawAction === "string") {
334
+ const a = rawAction.toLowerCase();
335
+ if (a === "create" || a === "update" || a === "delete") {
336
+ return a;
337
+ }
338
+ }
339
+ if (event === "record_change") return "update";
340
+ if (event === "create" || event === "record_create") return "create";
341
+ if (event === "delete" || event === "record_delete") return "delete";
342
+ return "update";
343
+ }
324
344
  var CollectionService = class {
325
345
  /** @internal */
326
- constructor(http, collectionName, authStore) {
346
+ constructor(http, collectionName, authStore, realtime) {
327
347
  this.http = http;
328
348
  this.collectionName = collectionName;
329
349
  this.authStore = authStore;
350
+ this.realtime = realtime;
330
351
  }
331
352
  encodeId(id) {
332
353
  return encodeURIComponent(id);
@@ -356,7 +377,8 @@ var CollectionService = class {
356
377
  }
357
378
  /**
358
379
  * Create a new record.
359
- * @param data Record fields.
380
+ * @param data Record fields. When `T` is a concrete shape (e.g. a generated
381
+ * record type), excess/unknown fields are rejected at compile time.
360
382
  * @param options Optional request options.
361
383
  */
362
384
  create(data, options) {
@@ -369,7 +391,8 @@ var CollectionService = class {
369
391
  /**
370
392
  * Update a record by ID.
371
393
  * @param id Record ID.
372
- * @param data Updated record fields.
394
+ * @param data Updated record fields. When `T` is a concrete shape, `data`
395
+ * must be a partial of `T` — unknown fields are rejected.
373
396
  * @param options Optional request options.
374
397
  */
375
398
  update(id, data, options) {
@@ -396,16 +419,73 @@ var CollectionService = class {
396
419
  * Useful for constructing `expand` query parameters.
397
420
  */
398
421
  async expandFields(options) {
399
- const data = await this.http.get(
400
- "/collections/" + this.encodeId(this.collectionName),
401
- options
402
- );
422
+ const data = await this.http.get("/collections/" + this.encodeId(this.collectionName), options);
403
423
  if (!data?.fields) return null;
404
424
  return data.fields.filter((f) => f.type === "relation" && f.options?.collection).map((f) => ({
405
425
  field: f.name,
406
426
  targetCollection: f.options.collection
407
427
  }));
408
428
  }
429
+ /**
430
+ * Cast this service to a specific record shape.
431
+ * Use when you have a hand-written or generated interface for the
432
+ * collection and want compile-time checking of create/update/list.
433
+ *
434
+ * @example
435
+ * ```ts
436
+ * interface Post {
437
+ * id: string;
438
+ * title: string;
439
+ * published: boolean;
440
+ * }
441
+ * const posts = client.collection("posts").typed<Post>();
442
+ * await posts.create({ title: "Hi", published: true }); // ✓
443
+ * await posts.create({ nope: 1 }); // ✗ compile error
444
+ * ```
445
+ */
446
+ typed() {
447
+ return this;
448
+ }
449
+ // ── Realtime Subscriptions (PocketBase-style) ──
450
+ /**
451
+ * Subscribe to realtime changes for this collection.
452
+ * The event's `action` is one of `"create" | "update" | "delete"`.
453
+ *
454
+ * Access is governed by the collection's `listRule` (PocketBase semantics):
455
+ * public collections allow anonymous subscriptions; other collections
456
+ * require a matching logged-in user or superuser.
457
+ *
458
+ * @param callback Received on every record change.
459
+ * @param recordId Optional — subscribe to a single record instead of `*`.
460
+ * @returns A function that unsubscribes this callback.
461
+ */
462
+ subscribe(callback, recordId) {
463
+ if (!this.realtime) {
464
+ console.warn("[lazypock] No realtime service configured.");
465
+ return () => {
466
+ };
467
+ }
468
+ const topic = "collection:" + this.collectionName + (recordId ? ":" + recordId : "");
469
+ const handler = (raw) => {
470
+ const record = raw.payload?.["record"] ?? {};
471
+ callback({
472
+ action: normalizeAction(raw.event, raw.payload?.["action"]),
473
+ record,
474
+ topic: raw.topic
475
+ });
476
+ };
477
+ this.realtime.ensureConnected();
478
+ this.realtime.subscribe(topic, handler);
479
+ return () => this.realtime?.unsubscribe(topic, handler);
480
+ }
481
+ /**
482
+ * Unsubscribe all callbacks from this collection (or a specific record).
483
+ * @param recordId Optional record id; omitting it unsubs everything.
484
+ */
485
+ unsubscribe(recordId) {
486
+ const topic = "collection:" + this.collectionName + (recordId ? ":" + recordId : "");
487
+ this.realtime?.unsubscribe(topic);
488
+ }
409
489
  // ── Auth Collection Methods ──
410
490
  /**
411
491
  * Authenticate with email/password against this auth collection.
@@ -442,6 +522,7 @@ var CollectionService = class {
442
522
  /**
443
523
  * Get available auth methods for this collection.
444
524
  */
525
+ // ── end Realtime ──
445
526
  async authMethods(options) {
446
527
  return this.http.get(
447
528
  "/" + this.encodeId(this.collectionName) + "/auth-methods",
@@ -472,6 +553,39 @@ var RealtimeService = class {
472
553
  // ── Heartbeat ──
473
554
  this.heartbeatInterval = null;
474
555
  }
556
+ /** Whether the WebSocket is currently open. */
557
+ get isOpen() {
558
+ return this.ws?.readyState === WebSocket.OPEN;
559
+ }
560
+ /**
561
+ * The most recently used WebSocket URL (set on {@link connect}).
562
+ * Useful for SDK convenience methods that auto-connect before subscribing.
563
+ */
564
+ get lastUrl() {
565
+ return this.url;
566
+ }
567
+ /** The auth token configured for this connection (set on connect). */
568
+ get lastToken() {
569
+ return this.token;
570
+ }
571
+ /**
572
+ * Set the socket URL. Useful before subscribing so the SDK can
573
+ * auto-connect on the first {@link subscribe}.
574
+ */
575
+ setUrl(url) {
576
+ this.url = url;
577
+ }
578
+ /*
579
+ * Ensure the socket is connected, then subscribe.
580
+ * Used by collection-level convenience wrappers so a connection is opened
581
+ * automatically on the first subscribe (matching PocketBase behaviour —
582
+ * works for anonymous/public collections too).
583
+ */
584
+ ensureConnected() {
585
+ if (this.isOpen || !this.url) return;
586
+ if (typeof WebSocket === "undefined") return;
587
+ this.doConnect();
588
+ }
475
589
  connect(opts) {
476
590
  this.url = opts.url;
477
591
  this.token = opts.token;
@@ -694,7 +808,189 @@ var FilesService = class {
694
808
  }
695
809
  };
696
810
 
697
- // src/index.ts
811
+ // src/typegen.ts
812
+ function fieldTypeScriptType(field, fallback = "unknown") {
813
+ const opts = field.options ?? {};
814
+ switch (field.type) {
815
+ case "text":
816
+ case "email":
817
+ case "url":
818
+ case "editor":
819
+ case "date":
820
+ case "datetime":
821
+ return "string";
822
+ case "number":
823
+ return "number";
824
+ case "bool":
825
+ return "boolean";
826
+ case "select": {
827
+ const values = Array.isArray(opts.values) ? opts.values : [];
828
+ if (values.length > 0) {
829
+ return values.map((v) => JSON.stringify(String(v))).join(" | ");
830
+ }
831
+ return "string";
832
+ }
833
+ case "multi_select": {
834
+ const values = Array.isArray(opts.values) ? opts.values : [];
835
+ if (values.length > 0) {
836
+ return `(${values.map((v) => JSON.stringify(String(v))).join(" | ")})[]`;
837
+ }
838
+ return "string[]";
839
+ }
840
+ case "file":
841
+ return "string";
842
+ case "multi_file":
843
+ return "string[]";
844
+ case "json":
845
+ case "geo":
846
+ return "Record<string, unknown>";
847
+ case "relation":
848
+ return (opts.maxSelect ?? 1) > 1 ? "string[]" : "string";
849
+ case "password":
850
+ return "never";
851
+ default:
852
+ return fallback;
853
+ }
854
+ }
855
+ function fieldTypeKind(field) {
856
+ const opts = field.options ?? {};
857
+ switch (field.type) {
858
+ case "text":
859
+ case "email":
860
+ case "url":
861
+ case "editor":
862
+ case "date":
863
+ case "datetime":
864
+ case "select":
865
+ case "file":
866
+ return "string";
867
+ case "number":
868
+ return "number";
869
+ case "bool":
870
+ return "boolean";
871
+ case "multi_select":
872
+ case "multi_file":
873
+ return "string-array";
874
+ case "json":
875
+ case "geo":
876
+ return "json";
877
+ case "relation":
878
+ return (opts.maxSelect ?? 1) > 1 ? "relation-many" : "relation";
879
+ case "password":
880
+ return "password";
881
+ default:
882
+ return "unknown";
883
+ }
884
+ }
885
+ function schemaFieldType(field) {
886
+ switch (fieldTypeKind(field)) {
887
+ case "string":
888
+ return String;
889
+ case "number":
890
+ return Number;
891
+ case "boolean":
892
+ return Boolean;
893
+ case "string-array":
894
+ return [String];
895
+ case "relation":
896
+ return String;
897
+ case "relation-many":
898
+ return [String];
899
+ case "json":
900
+ return Object;
901
+ case "password":
902
+ return void 0;
903
+ case "unknown":
904
+ return void 0;
905
+ }
906
+ }
907
+
908
+ // src/codegen.ts
909
+ function collectionTypeName(name) {
910
+ return name.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("").replace(/^[0-9]/, "_$&");
911
+ }
912
+ function fieldKey(name) {
913
+ return name.replace(/[^a-zA-Z0-9_]/g, "_");
914
+ }
915
+ function generateTypes(collections, options = {}) {
916
+ const {
917
+ packageName = "lazypock",
918
+ includeBaseFields = true,
919
+ skipSystem = false
920
+ } = options;
921
+ const filtered = skipSystem ? collections.filter(
922
+ (c) => !c.system && !c.name.startsWith("_") && c.name !== "users"
923
+ ) : collections;
924
+ const sections = [];
925
+ sections.push(`// \u2500\u2500 Auto-generated by lazypock-ts \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
926
+ // Do not edit by hand. Regenerate with: npx lazypock-gen
927
+ // Schema snapshot: ${(/* @__PURE__ */ new Date()).toISOString()}`);
928
+ if (includeBaseFields) {
929
+ sections.push(`export interface BaseRecord {
930
+ id: string;
931
+ collectionId: string;
932
+ collectionName: string;
933
+ created: string;
934
+ updated: string;
935
+ }`);
936
+ }
937
+ for (const coll of filtered) {
938
+ const typeName = collectionTypeName(coll.name);
939
+ const fields = coll.fields ?? [];
940
+ const lines = fields.map((f) => memberLine(f)).filter((l) => l !== "");
941
+ const body = lines.join("\n");
942
+ sections.push(
943
+ `export interface ${typeName}Record${renderInterface({
944
+ extends: includeBaseFields ? "BaseRecord" : void 0,
945
+ body
946
+ })}`
947
+ );
948
+ }
949
+ sections.push(`export interface AuthRecord extends BaseRecord {
950
+ email: string;
951
+ verified: boolean;
952
+ }`);
953
+ const mapEntries = filtered.map(
954
+ (c) => ` "${c.name}": ${collectionTypeName(c.name)}Record${c.type === "auth" ? " & AuthRecord" : ""};`
955
+ ).join("\n");
956
+ sections.push(`export interface LazypockCollections {
957
+ ${mapEntries}
958
+ }`);
959
+ sections.push(`import { LazypockClient, type LazypockClientOptions, type CollectionService } from "${packageName}";
960
+
961
+ /**
962
+ * Create a Lazypock client typed against this schema snapshot.
963
+ * Collection access is fully type-checked:
964
+ * client.collection("posts").create({ title: "x" }) // title must exist
965
+ */
966
+ export function createClient(options: LazypockClientOptions): TypedClient {
967
+ return new TypedClient(options);
968
+ }
969
+
970
+ export class TypedClient extends LazypockClient {
971
+ override collection<K extends keyof LazypockCollections>(name: K): CollectionService<LazypockCollections[K]>;
972
+ override collection(name: string): CollectionService<unknown> {
973
+ return super.collection(name) as unknown as CollectionService<unknown>;
974
+ }
975
+ }`);
976
+ return sections.join("\n\n") + "\n";
977
+ }
978
+ function renderInterface(opts) {
979
+ const ext = opts.extends ? ` extends ${opts.extends}` : "";
980
+ if (!opts.body) return `${ext} {}`;
981
+ return `${ext} {
982
+ ${opts.body}
983
+ }`;
984
+ }
985
+ function memberLine(f) {
986
+ const key = fieldKey(f.name);
987
+ const req = f.required || f.type === "password" ? "" : "?";
988
+ const type = fieldTypeScriptType(f);
989
+ if (type === "never") return "";
990
+ return ` ${JSON.stringify(key)}${req}: ${type};`;
991
+ }
992
+
993
+ // src/lazypock.ts
698
994
  var LazypockClient = class {
699
995
  /**
700
996
  * Create a new Lazypock client.
@@ -706,23 +1002,59 @@ var LazypockClient = class {
706
1002
  this.authStore = options.authStore ?? new AuthStore(options.storage ?? memoryStorage);
707
1003
  this.http = new HttpClient(baseUrl, this.authStore);
708
1004
  this.realtime = options.realtime ?? new RealtimeService();
1005
+ if (!options.realtime) {
1006
+ this.realtime.setUrl(wsUrlFromBaseUrl(baseUrl));
1007
+ }
709
1008
  this.files = new FilesService(this.http);
1009
+ if (options.types?.schemas) {
1010
+ this.schemaByName = new Map(
1011
+ options.types.schemas.map((s) => [s.name, s])
1012
+ );
1013
+ }
710
1014
  }
711
1015
  /**
712
- * Get or create a typed service for the given collection.
1016
+ * Get or create a service for the given collection.
713
1017
  * Services are cached after first access.
714
1018
  *
1019
+ * For typed CRUD, either:
1020
+ * - cast at the call site: `client.collection("posts").typed<Post>()`
1021
+ * - or use the typed factory: `createClient<{ posts: Post }>()`
1022
+ *
715
1023
  * @param name The collection name.
716
1024
  * @returns A {@link CollectionService} instance.
717
1025
  */
718
1026
  collection(name) {
719
1027
  let svc = this.collectionCache.get(name);
720
1028
  if (!svc) {
721
- svc = new CollectionService(this.http, name, this.authStore);
1029
+ svc = new CollectionService(
1030
+ this.http,
1031
+ name,
1032
+ this.authStore,
1033
+ this.realtime
1034
+ );
722
1035
  this.collectionCache.set(name, svc);
723
1036
  }
724
1037
  return svc;
725
1038
  }
1039
+ /**
1040
+ * Get a typed service whose record shape is derived from the schema
1041
+ * passed via `options.types.schemas` (if available), or fall back to
1042
+ * the untyped service otherwise.
1043
+ *
1044
+ * @experimental
1045
+ */
1046
+ collectionFor(name) {
1047
+ return this.collection(name);
1048
+ }
1049
+ /**
1050
+ * Generate TypeScript types from the schemas provided to this client
1051
+ * (via `options.types.schemas`). Returns a string ready to write to a
1052
+ * `lazypock.types.ts` file.
1053
+ */
1054
+ generateTypes(options) {
1055
+ const schemas = this.schemaByName ? [...this.schemaByName.values()] : [];
1056
+ return generateTypes(schemas, options);
1057
+ }
726
1058
  // ── Auth ──
727
1059
  /** Check whether any superuser exists (for login vs setup screen routing). */
728
1060
  async checkSuperuser() {
@@ -958,14 +1290,32 @@ var LazypockClient = class {
958
1290
  );
959
1291
  }
960
1292
  };
1293
+
1294
+ // src/client.ts
1295
+ var TypedClient = class extends LazypockClient {
1296
+ collection(name) {
1297
+ return super.collection(name);
1298
+ }
1299
+ };
1300
+ function createClient(options) {
1301
+ return new TypedClient(options);
1302
+ }
961
1303
  // Annotate the CommonJS export names for ESM import in node:
962
1304
  0 && (module.exports = {
963
1305
  ApiError,
964
1306
  AuthStore,
1307
+ CollectionService,
965
1308
  FilesService,
966
1309
  LazypockClient,
967
1310
  RealtimeService,
1311
+ TypedClient,
1312
+ collectionTypeName,
1313
+ createClient,
1314
+ fieldTypeKind,
1315
+ fieldTypeScriptType,
1316
+ generateTypes,
968
1317
  getFileUrl,
1318
+ schemaFieldType,
969
1319
  wsUrlFromBaseUrl
970
1320
  });
971
1321
  //# sourceMappingURL=index.cjs.map