lazypock 0.12.0 → 0.14.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 +137 -61
- package/dist/index.cjs +763 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +353 -15
- package/dist/index.d.ts +353 -15
- package/dist/index.global.js +761 -45
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +760 -44
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/collection.ts +562 -57
- package/src/filter.ts +193 -0
- package/src/http.ts +7 -2
- package/src/index.ts +12 -0
- package/src/lazypock.ts +22 -0
- package/src/oauth2.ts +397 -0
- package/src/types.ts +44 -12
package/dist/index.cjs
CHANGED
|
@@ -25,6 +25,8 @@ __export(index_exports, {
|
|
|
25
25
|
CollectionService: () => CollectionService,
|
|
26
26
|
CollectionsService: () => CollectionsService,
|
|
27
27
|
FilesService: () => FilesService,
|
|
28
|
+
FilterBuilder: () => FilterBuilder,
|
|
29
|
+
FilterExpr: () => FilterExpr,
|
|
28
30
|
HttpClient: () => HttpClient,
|
|
29
31
|
LazypockClient: () => LazypockClient,
|
|
30
32
|
RealtimeService: () => RealtimeService,
|
|
@@ -44,12 +46,13 @@ module.exports = __toCommonJS(index_exports);
|
|
|
44
46
|
|
|
45
47
|
// src/types.ts
|
|
46
48
|
var ApiError = class extends Error {
|
|
47
|
-
constructor(message, data, status, isAbort = false) {
|
|
49
|
+
constructor(message, data, status, isAbort = false, code) {
|
|
48
50
|
super(message);
|
|
49
51
|
this.name = "ApiError";
|
|
50
52
|
this.data = data;
|
|
51
53
|
this.status = status;
|
|
52
54
|
this.isAbort = isAbort;
|
|
55
|
+
this.code = code;
|
|
53
56
|
}
|
|
54
57
|
};
|
|
55
58
|
|
|
@@ -80,11 +83,15 @@ var HttpClient = class {
|
|
|
80
83
|
this.inflight = {};
|
|
81
84
|
/** Global toggle for the auto-cancellation behaviour (default: on). */
|
|
82
85
|
this.enableAutoCancellation = true;
|
|
83
|
-
this.
|
|
86
|
+
this._baseUrl = baseUrl.replace(/\/+$/, "");
|
|
84
87
|
this.authStore = authStore;
|
|
85
88
|
this.connectionId = connectionId;
|
|
86
89
|
this.defaultFetch = globalThis.fetch.bind(globalThis);
|
|
87
90
|
}
|
|
91
|
+
/** The API base URL (trailing slash stripped). Used for OAuth2 origin checks. */
|
|
92
|
+
get baseUrl() {
|
|
93
|
+
return this._baseUrl;
|
|
94
|
+
}
|
|
88
95
|
async refreshAuth() {
|
|
89
96
|
const collection = this.authStore.collectionName;
|
|
90
97
|
if (!collection) return null;
|
|
@@ -470,6 +477,340 @@ var memoryStorage = {
|
|
|
470
477
|
}
|
|
471
478
|
};
|
|
472
479
|
|
|
480
|
+
// src/filter.ts
|
|
481
|
+
function literal(value) {
|
|
482
|
+
if (value === null) return "null";
|
|
483
|
+
if (typeof value === "number") {
|
|
484
|
+
if (!Number.isFinite(value)) {
|
|
485
|
+
throw new Error(
|
|
486
|
+
`[lazypock] filter: ${String(value)} is not a valid numeric value`
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
return String(value);
|
|
490
|
+
}
|
|
491
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
492
|
+
const escaped = String(value).replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
493
|
+
return `'${escaped}'`;
|
|
494
|
+
}
|
|
495
|
+
var FilterExpr = class _FilterExpr {
|
|
496
|
+
/** @internal Build from a raw string — use {@link FilterBuilder} instead. */
|
|
497
|
+
constructor(raw) {
|
|
498
|
+
this.raw = raw;
|
|
499
|
+
}
|
|
500
|
+
/** Combine with `&&` (both must match). */
|
|
501
|
+
and(other) {
|
|
502
|
+
return new _FilterExpr(`(${this.raw} && ${other.raw})`);
|
|
503
|
+
}
|
|
504
|
+
/** Combine with `||` (either may match). */
|
|
505
|
+
or(other) {
|
|
506
|
+
return new _FilterExpr(`(${this.raw} || ${other.raw})`);
|
|
507
|
+
}
|
|
508
|
+
/** Negate the expression (`!(…)`). */
|
|
509
|
+
not() {
|
|
510
|
+
return new _FilterExpr(`!(${this.raw})`);
|
|
511
|
+
}
|
|
512
|
+
/** The raw filter string (used when serializing the request). */
|
|
513
|
+
toString() {
|
|
514
|
+
return this.raw;
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
var FilterBuilder = class {
|
|
518
|
+
/** @internal Use {@link CollectionService.where}. */
|
|
519
|
+
constructor(field) {
|
|
520
|
+
this.field = field;
|
|
521
|
+
}
|
|
522
|
+
/** `field = value` (equal). */
|
|
523
|
+
eq(value) {
|
|
524
|
+
return this.cmp("=", value);
|
|
525
|
+
}
|
|
526
|
+
/** `field != value` (not equal). */
|
|
527
|
+
neq(value) {
|
|
528
|
+
return this.cmp("!=", value);
|
|
529
|
+
}
|
|
530
|
+
/** `field ~ value` (contains / LIKE). */
|
|
531
|
+
contains(value) {
|
|
532
|
+
return this.cmp("~", value);
|
|
533
|
+
}
|
|
534
|
+
/** `field !~ value` (does not contain / NOT LIKE). */
|
|
535
|
+
notContains(value) {
|
|
536
|
+
return this.cmp("!~", value);
|
|
537
|
+
}
|
|
538
|
+
/** `field > value` (greater than). */
|
|
539
|
+
gt(value) {
|
|
540
|
+
return this.cmp(">", value);
|
|
541
|
+
}
|
|
542
|
+
/** `field >= value` (greater than or equal). */
|
|
543
|
+
gte(value) {
|
|
544
|
+
return this.cmp(">=", value);
|
|
545
|
+
}
|
|
546
|
+
/** `field < value` (less than). */
|
|
547
|
+
lt(value) {
|
|
548
|
+
return this.cmp("<", value);
|
|
549
|
+
}
|
|
550
|
+
/** `field <= value` (less than or equal). */
|
|
551
|
+
lte(value) {
|
|
552
|
+
return this.cmp("<=", value);
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* `field = v1 || field = v2 || …` (value is in the list).
|
|
556
|
+
*
|
|
557
|
+
* ```ts
|
|
558
|
+
* svc.where("id").in(["a", "b", "c"]);
|
|
559
|
+
* // → (id = 'a' || id = 'b' || id = 'c')
|
|
560
|
+
* ```
|
|
561
|
+
*/
|
|
562
|
+
in(values) {
|
|
563
|
+
return new FilterExpr(this.chain("=", " || ", values));
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* `field != v1 && field != v2 && …` (value is not in the list).
|
|
567
|
+
*/
|
|
568
|
+
notIn(values) {
|
|
569
|
+
return new FilterExpr(this.chain("!=", " && ", values));
|
|
570
|
+
}
|
|
571
|
+
/** Build `(field <op> v <join> field <op> v …)`, wrapped for safe composing. */
|
|
572
|
+
chain(op, join, values) {
|
|
573
|
+
if (values.length === 0) {
|
|
574
|
+
throw new Error(
|
|
575
|
+
`[lazypock] filter: ${op === "=" ? "in" : "notIn"}() requires at least one value`
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
const clauses = values.map((v) => `${this.field} ${op} ${literal(v)}`);
|
|
579
|
+
return `(${clauses.join(join)})`;
|
|
580
|
+
}
|
|
581
|
+
/** `field ?= value` — at least one array element equals `value`. */
|
|
582
|
+
anyEq(value) {
|
|
583
|
+
return this.cmp("?=", value);
|
|
584
|
+
}
|
|
585
|
+
/** `field ?!= value` — at least one array element differs. */
|
|
586
|
+
anyNeq(value) {
|
|
587
|
+
return this.cmp("?!=", value);
|
|
588
|
+
}
|
|
589
|
+
/** `field ?~ value` — at least one array element contains `value`. */
|
|
590
|
+
anyContains(value) {
|
|
591
|
+
return this.cmp("?~", value);
|
|
592
|
+
}
|
|
593
|
+
/** `field ?!~ value` — at least one array element does not contain `value`. */
|
|
594
|
+
anyNotContains(value) {
|
|
595
|
+
return this.cmp("?!~", value);
|
|
596
|
+
}
|
|
597
|
+
/** `field ?> value` — at least one array element is greater. */
|
|
598
|
+
anyGt(value) {
|
|
599
|
+
return this.cmp("?>", value);
|
|
600
|
+
}
|
|
601
|
+
/** `field ?>= value` — at least one array element is greater or equal. */
|
|
602
|
+
anyGte(value) {
|
|
603
|
+
return this.cmp("?>=", value);
|
|
604
|
+
}
|
|
605
|
+
/** `field ?< value` — at least one array element is less. */
|
|
606
|
+
anyLt(value) {
|
|
607
|
+
return this.cmp("?<", value);
|
|
608
|
+
}
|
|
609
|
+
/** `field ?<= value` — at least one array element is less or equal. */
|
|
610
|
+
anyLte(value) {
|
|
611
|
+
return this.cmp("?<=", value);
|
|
612
|
+
}
|
|
613
|
+
cmp(op, value) {
|
|
614
|
+
return new FilterExpr(`${this.field} ${op} ${literal(value)}`);
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
// src/oauth2.ts
|
|
619
|
+
var DEFAULT_POPUP_WIDTH = 500;
|
|
620
|
+
var DEFAULT_POPUP_HEIGHT = 700;
|
|
621
|
+
var DEFAULT_OAUTH2_TIMEOUT_MS = 12e4;
|
|
622
|
+
var CLOSE_POLL_MS = 500;
|
|
623
|
+
function oauth2RedirectOrigin(baseUrl) {
|
|
624
|
+
try {
|
|
625
|
+
return new URL(baseUrl).origin;
|
|
626
|
+
} catch {
|
|
627
|
+
throw new ApiError(
|
|
628
|
+
`Invalid baseUrl for OAuth2 origin check: "${baseUrl}"`,
|
|
629
|
+
{},
|
|
630
|
+
0,
|
|
631
|
+
false,
|
|
632
|
+
"oauth2_invalid_base_url"
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
function isSafeBrowserUrl(url) {
|
|
637
|
+
try {
|
|
638
|
+
const protocol = new URL(url).protocol;
|
|
639
|
+
return protocol === "http:" || protocol === "https:";
|
|
640
|
+
} catch {
|
|
641
|
+
return false;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
function isRecordAuth(value) {
|
|
645
|
+
return !!value && typeof value === "object" && typeof value.token === "string" && !!value.record && typeof value.record === "object";
|
|
646
|
+
}
|
|
647
|
+
function authorizeWithOAuth2Popup(opts) {
|
|
648
|
+
return new Promise((resolve, reject) => {
|
|
649
|
+
if (typeof window === "undefined") {
|
|
650
|
+
reject(
|
|
651
|
+
new ApiError(
|
|
652
|
+
"OAuth2 popup flow requires a browser window. Use `authWithOAuth2Code()` in non-browser environments.",
|
|
653
|
+
{},
|
|
654
|
+
0,
|
|
655
|
+
false,
|
|
656
|
+
"oauth2_popup_unavailable"
|
|
657
|
+
)
|
|
658
|
+
);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
let settled = false;
|
|
662
|
+
let popup = null;
|
|
663
|
+
let closePoll = null;
|
|
664
|
+
let timeout = null;
|
|
665
|
+
const cleanup = () => {
|
|
666
|
+
if (timeout) {
|
|
667
|
+
clearTimeout(timeout);
|
|
668
|
+
timeout = null;
|
|
669
|
+
}
|
|
670
|
+
if (closePoll) {
|
|
671
|
+
clearInterval(closePoll);
|
|
672
|
+
closePoll = null;
|
|
673
|
+
}
|
|
674
|
+
window.removeEventListener("message", onMessage);
|
|
675
|
+
};
|
|
676
|
+
const finish = (err, result) => {
|
|
677
|
+
if (settled) return;
|
|
678
|
+
settled = true;
|
|
679
|
+
cleanup();
|
|
680
|
+
if (err) reject(err);
|
|
681
|
+
else resolve(result);
|
|
682
|
+
};
|
|
683
|
+
const onMessage = (event) => {
|
|
684
|
+
if (event.origin !== opts.expectedOrigin) return;
|
|
685
|
+
if (popup && event.source && event.source !== popup) return;
|
|
686
|
+
const data = event.data;
|
|
687
|
+
if (!data || typeof data !== "object") return;
|
|
688
|
+
if (data.type === "lazypock:oauth2") {
|
|
689
|
+
if (isRecordAuth(data.result)) {
|
|
690
|
+
if (popup) {
|
|
691
|
+
try {
|
|
692
|
+
popup.close();
|
|
693
|
+
} catch {
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
finish(null, data.result);
|
|
697
|
+
} else {
|
|
698
|
+
finish(
|
|
699
|
+
new ApiError(
|
|
700
|
+
"OAuth2 callback returned an invalid result.",
|
|
701
|
+
data,
|
|
702
|
+
400,
|
|
703
|
+
false,
|
|
704
|
+
"oauth2_provider_error"
|
|
705
|
+
)
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
if (data.type === "lazypock:oauth2:error") {
|
|
711
|
+
const result = data.result ?? {};
|
|
712
|
+
const message = typeof result.message === "string" && result.message ? result.message : "OAuth2 authorization failed.";
|
|
713
|
+
finish(
|
|
714
|
+
new ApiError(
|
|
715
|
+
message,
|
|
716
|
+
result,
|
|
717
|
+
typeof result.code === "number" ? result.code : 400,
|
|
718
|
+
false,
|
|
719
|
+
"oauth2_provider_error"
|
|
720
|
+
)
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
const openPopup = () => {
|
|
725
|
+
if (!isSafeBrowserUrl(opts.authURL)) {
|
|
726
|
+
finish(
|
|
727
|
+
new ApiError(
|
|
728
|
+
"OAuth2 authorization URL has an unsupported protocol.",
|
|
729
|
+
{},
|
|
730
|
+
0,
|
|
731
|
+
false,
|
|
732
|
+
"oauth2_provider_error"
|
|
733
|
+
)
|
|
734
|
+
);
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
const width = opts.popup?.width ?? DEFAULT_POPUP_WIDTH;
|
|
738
|
+
const height = opts.popup?.height ?? DEFAULT_POPUP_HEIGHT;
|
|
739
|
+
const features = "width=" + width + ",height=" + height + ",scrollbars=yes,resizable=yes";
|
|
740
|
+
const win = window.open(opts.authURL, "_blank", features);
|
|
741
|
+
if (!win) {
|
|
742
|
+
finish(
|
|
743
|
+
new ApiError(
|
|
744
|
+
"OAuth2 popup was blocked by the browser. Please allow popups for this site and try again.",
|
|
745
|
+
{},
|
|
746
|
+
0,
|
|
747
|
+
false,
|
|
748
|
+
"oauth2_popup_blocked"
|
|
749
|
+
)
|
|
750
|
+
);
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
popup = win;
|
|
754
|
+
closePoll = setInterval(() => {
|
|
755
|
+
if (popup && popup.closed) {
|
|
756
|
+
finish(
|
|
757
|
+
new ApiError(
|
|
758
|
+
"OAuth2 sign-in was cancelled.",
|
|
759
|
+
{},
|
|
760
|
+
0,
|
|
761
|
+
false,
|
|
762
|
+
"oauth2_cancelled"
|
|
763
|
+
)
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
}, CLOSE_POLL_MS);
|
|
767
|
+
};
|
|
768
|
+
window.addEventListener("message", onMessage);
|
|
769
|
+
timeout = setTimeout(() => {
|
|
770
|
+
if (popup) {
|
|
771
|
+
try {
|
|
772
|
+
popup.close();
|
|
773
|
+
} catch {
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
finish(
|
|
777
|
+
new ApiError(
|
|
778
|
+
"OAuth2 sign-in timed out.",
|
|
779
|
+
{},
|
|
780
|
+
0,
|
|
781
|
+
false,
|
|
782
|
+
"oauth2_timeout"
|
|
783
|
+
)
|
|
784
|
+
);
|
|
785
|
+
}, opts.timeoutMs);
|
|
786
|
+
if (opts.urlCallback) {
|
|
787
|
+
Promise.resolve(opts.urlCallback(opts.authURL)).catch((err) => {
|
|
788
|
+
finish(
|
|
789
|
+
err instanceof ApiError ? err : new ApiError(
|
|
790
|
+
`OAuth2 urlCallback failed: ${String(err)}`,
|
|
791
|
+
{},
|
|
792
|
+
0,
|
|
793
|
+
false,
|
|
794
|
+
"oauth2_provider_error"
|
|
795
|
+
)
|
|
796
|
+
);
|
|
797
|
+
});
|
|
798
|
+
} else if (typeof window.open === "function") {
|
|
799
|
+
openPopup();
|
|
800
|
+
} else {
|
|
801
|
+
finish(
|
|
802
|
+
new ApiError(
|
|
803
|
+
"OAuth2 popup flow requires a browser window. Use `authWithOAuth2Code()` in non-browser environments.",
|
|
804
|
+
{},
|
|
805
|
+
0,
|
|
806
|
+
false,
|
|
807
|
+
"oauth2_popup_unavailable"
|
|
808
|
+
)
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
|
|
473
814
|
// src/collection.ts
|
|
474
815
|
function stableStringify(value) {
|
|
475
816
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -507,14 +848,24 @@ function normalizeAction(event, rawAction) {
|
|
|
507
848
|
if (event === "delete" || event === "record_delete") return "delete";
|
|
508
849
|
return "update";
|
|
509
850
|
}
|
|
851
|
+
function pickFields(value, fields) {
|
|
852
|
+
if (!value || typeof value !== "object") return {};
|
|
853
|
+
const src = value;
|
|
854
|
+
const out = {};
|
|
855
|
+
for (const f of fields) {
|
|
856
|
+
if (f in src) out[f] = src[f];
|
|
857
|
+
}
|
|
858
|
+
return out;
|
|
859
|
+
}
|
|
510
860
|
var CollectionService = class _CollectionService {
|
|
511
861
|
/** @internal */
|
|
512
|
-
constructor(http, collectionName, authStore, realtime, schema) {
|
|
862
|
+
constructor(http, collectionName, authStore, realtime, schema, schemaResolver) {
|
|
513
863
|
this.http = http;
|
|
514
864
|
this.collectionName = collectionName;
|
|
515
865
|
this.authStore = authStore;
|
|
516
866
|
this.realtime = realtime;
|
|
517
867
|
this.schema = schema;
|
|
868
|
+
this.schemaResolver = schemaResolver;
|
|
518
869
|
}
|
|
519
870
|
encodeId(id) {
|
|
520
871
|
return encodeURIComponent(id);
|
|
@@ -549,7 +900,8 @@ var CollectionService = class _CollectionService {
|
|
|
549
900
|
this.collectionName,
|
|
550
901
|
this.authStore,
|
|
551
902
|
this.realtime,
|
|
552
|
-
this.schema
|
|
903
|
+
this.schema,
|
|
904
|
+
this.schemaResolver
|
|
553
905
|
);
|
|
554
906
|
derived.fieldsPreset = preset;
|
|
555
907
|
if (this.schema && preset !== "*") {
|
|
@@ -612,20 +964,255 @@ var CollectionService = class _CollectionService {
|
|
|
612
964
|
])
|
|
613
965
|
].join(",");
|
|
614
966
|
}
|
|
967
|
+
// ── Typed filter builder ──
|
|
968
|
+
/**
|
|
969
|
+
* Start a type-checked filter expression. The field name is suggested
|
|
970
|
+
* from the collection's key set and the value is checked against the
|
|
971
|
+
* field's type (when the service is typed). Values are escaped safely.
|
|
972
|
+
*
|
|
973
|
+
* ```ts
|
|
974
|
+
* const q = posts.where;
|
|
975
|
+
* await posts.getList(1, 20, {
|
|
976
|
+
* filter: q("title").contains("x").and(q("published").eq(true)),
|
|
977
|
+
* });
|
|
978
|
+
* // → filter=(title ~ '%x%' && published = true)
|
|
979
|
+
*
|
|
980
|
+
* // PocketBase `?` array operators (multi-select / multi-relation):
|
|
981
|
+
* posts.getList(1, 20, { filter: q("tags").anyEq("news") });
|
|
982
|
+
* ```
|
|
983
|
+
*
|
|
984
|
+
* Chain `.and()` / `.or()` / `.not()` to compose. A `FilterExpr` can be
|
|
985
|
+
* passed anywhere a `filter` string is accepted. Relation dot-paths
|
|
986
|
+
* (`where("author.email")`) are supported by servers that compile
|
|
987
|
+
* relation dot-path filters, and `where("id").in(ids)` covers list
|
|
988
|
+
* membership without hand-written OR chains.
|
|
989
|
+
*/
|
|
990
|
+
where(field) {
|
|
991
|
+
return new FilterBuilder(field);
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* Resolve the leading relation chain of an expand token against the
|
|
995
|
+
* (optionally schema-aware) collection graph.
|
|
996
|
+
*
|
|
997
|
+
* Walks `segments` from this collection: each segment that is a relation
|
|
998
|
+
* advances to its target collection. Stops at the first non-relation
|
|
999
|
+
* segment or when the target schema is unknown.
|
|
1000
|
+
*
|
|
1001
|
+
* @returns `depth` — how many leading segments are relations; and
|
|
1002
|
+
* `canInspect` — whether the schema at `depth` is known (so the next
|
|
1003
|
+
* segment can be classified as a field selection vs a nested relation).
|
|
1004
|
+
*/
|
|
1005
|
+
resolveRelationChain(segments) {
|
|
1006
|
+
let currentFields = this.schema?.fields;
|
|
1007
|
+
if (!currentFields) return { depth: 0, canInspect: false };
|
|
1008
|
+
for (let i = 0; i < segments.length; i++) {
|
|
1009
|
+
const field = currentFields.find((f) => f.name === segments[i]);
|
|
1010
|
+
if (!field || field.type !== "relation") {
|
|
1011
|
+
return { depth: i, canInspect: true };
|
|
1012
|
+
}
|
|
1013
|
+
if (i === segments.length - 1) {
|
|
1014
|
+
return { depth: segments.length, canInspect: true };
|
|
1015
|
+
}
|
|
1016
|
+
const target = this.resolveTargetCollection(field);
|
|
1017
|
+
if (!target?.fields) {
|
|
1018
|
+
return { depth: i + 1, canInspect: false };
|
|
1019
|
+
}
|
|
1020
|
+
currentFields = target.fields;
|
|
1021
|
+
}
|
|
1022
|
+
return { depth: segments.length, canInspect: true };
|
|
1023
|
+
}
|
|
1024
|
+
/** Follow a relation field to its target collection, if resolvable. */
|
|
1025
|
+
resolveTargetCollection(field) {
|
|
1026
|
+
const key = field.options?.collection ?? field.options?.collectionId ?? field.collectionId ?? void 0;
|
|
1027
|
+
if (!key || !this.schemaResolver) return void 0;
|
|
1028
|
+
return this.schemaResolver(String(key));
|
|
1029
|
+
}
|
|
615
1030
|
/** Warn once when an expand field is not a relation (schema known). */
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
this.schema.fields.filter((f) => f.type === "relation").map((f) => f.name)
|
|
1031
|
+
warnNonRelation(name) {
|
|
1032
|
+
console.warn(
|
|
1033
|
+
`[lazypock] expand("${name}"): field is not a relation on collection "${this.collectionName}"`
|
|
620
1034
|
);
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Validate and normalise a caller's `expand` string.
|
|
1038
|
+
*
|
|
1039
|
+
* PocketBase only expands *relations*: a dotted token like `author.user`
|
|
1040
|
+
* walks into the related record. Callers often use a dotted token to mean
|
|
1041
|
+
* "only these fields of the expanded record" (`owner.name,owner.email`),
|
|
1042
|
+
* which PocketBase silently drops. When the trailing segment is not a
|
|
1043
|
+
* relation — resolved from the schema graph — the token is rewritten as a
|
|
1044
|
+
* field selection:
|
|
1045
|
+
*
|
|
1046
|
+
* ```
|
|
1047
|
+
* expand: 'owner.name,owner.email'
|
|
1048
|
+
* → expand=owner&fields=<base>,expand.owner.name,expand.owner.email
|
|
1049
|
+
* ```
|
|
1050
|
+
*
|
|
1051
|
+
* Tokens whose whole path is relations keep their existing nested-expand
|
|
1052
|
+
* behaviour. Without a schema the two are indistinguishable, so a lone
|
|
1053
|
+
* dotted token stays a nested expand (with a warning) while two or more
|
|
1054
|
+
* dotted tokens sharing the same relation are treated as field selection.
|
|
1055
|
+
*/
|
|
1056
|
+
normalizeExpand(expand) {
|
|
1057
|
+
const narrow = /* @__PURE__ */ new Map();
|
|
1058
|
+
if (!expand) return { fieldSelectors: [], narrow };
|
|
1059
|
+
const tokens = expand.split(",").map((t) => t.trim()).filter(Boolean);
|
|
1060
|
+
if (tokens.length === 0) return { fieldSelectors: [], narrow };
|
|
1061
|
+
const relations = this.schema?.fields ? new Set(
|
|
1062
|
+
this.schema.fields.filter((f) => f.type === "relation").map((f) => f.name)
|
|
1063
|
+
) : void 0;
|
|
1064
|
+
const parsed = [];
|
|
1065
|
+
for (const token of tokens) {
|
|
1066
|
+
const segments = token.split(".").filter(Boolean);
|
|
1067
|
+
const base = segments[0] ?? token;
|
|
1068
|
+
if (segments.length <= 1) {
|
|
1069
|
+
if (relations && !relations.has(base)) {
|
|
1070
|
+
this.warnNonRelation(base);
|
|
1071
|
+
}
|
|
1072
|
+
parsed.push({ token, base, segments, kind: "plain", depth: 0 });
|
|
1073
|
+
continue;
|
|
1074
|
+
}
|
|
1075
|
+
if (!relations) {
|
|
1076
|
+
parsed.push({ token, base, segments, kind: "unknown", depth: 1 });
|
|
1077
|
+
continue;
|
|
1078
|
+
}
|
|
1079
|
+
const { depth, canInspect } = this.resolveRelationChain(segments);
|
|
1080
|
+
if (depth === 0) {
|
|
1081
|
+
this.warnNonRelation(base);
|
|
1082
|
+
parsed.push({ token, base, segments, kind: "plain", depth: 0 });
|
|
1083
|
+
} else if (depth === segments.length) {
|
|
1084
|
+
parsed.push({ token, base, segments, kind: "nested", depth });
|
|
1085
|
+
} else if (canInspect) {
|
|
1086
|
+
parsed.push({ token, base, segments, kind: "narrow", depth });
|
|
1087
|
+
} else {
|
|
1088
|
+
parsed.push({ token, base, segments, kind: "unknown", depth });
|
|
627
1089
|
}
|
|
628
1090
|
}
|
|
1091
|
+
const byBase = /* @__PURE__ */ new Map();
|
|
1092
|
+
for (const t of parsed) {
|
|
1093
|
+
const arr = byBase.get(t.base);
|
|
1094
|
+
if (arr) arr.push(t);
|
|
1095
|
+
else byBase.set(t.base, [t]);
|
|
1096
|
+
}
|
|
1097
|
+
for (const [base, group] of byBase) {
|
|
1098
|
+
const unknowns = group.filter((t) => t.kind === "unknown");
|
|
1099
|
+
if (unknowns.length === 0) continue;
|
|
1100
|
+
const distinct = new Set(unknowns.map((t) => t.token));
|
|
1101
|
+
const hasNarrow = group.some((t) => t.kind === "narrow");
|
|
1102
|
+
if (!hasNarrow && distinct.size < 2) {
|
|
1103
|
+
for (const t of unknowns) {
|
|
1104
|
+
t.kind = "nested";
|
|
1105
|
+
console.warn(
|
|
1106
|
+
`[lazypock] expand("${t.token}"): treated as a nested relation. Pass types.schemas (or use fields=) to select fields of "${base}".`
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1109
|
+
} else {
|
|
1110
|
+
for (const t of unknowns) t.kind = "narrow";
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
const expandParams = [];
|
|
1114
|
+
const expandSeen = /* @__PURE__ */ new Set();
|
|
1115
|
+
const fieldSelectors = [];
|
|
1116
|
+
const selectorSeen = /* @__PURE__ */ new Set();
|
|
1117
|
+
const pushParam = (p) => {
|
|
1118
|
+
if (!expandSeen.has(p)) {
|
|
1119
|
+
expandSeen.add(p);
|
|
1120
|
+
expandParams.push(p);
|
|
1121
|
+
}
|
|
1122
|
+
};
|
|
1123
|
+
const pushSelector = (s) => {
|
|
1124
|
+
if (!selectorSeen.has(s)) {
|
|
1125
|
+
selectorSeen.add(s);
|
|
1126
|
+
fieldSelectors.push(s);
|
|
1127
|
+
}
|
|
1128
|
+
};
|
|
1129
|
+
for (const t of parsed) {
|
|
1130
|
+
if (t.kind === "plain") {
|
|
1131
|
+
pushParam(t.token);
|
|
1132
|
+
pushSelector(`expand.${t.token}`);
|
|
1133
|
+
} else if (t.kind === "nested") {
|
|
1134
|
+
for (let i = 1; i <= t.segments.length; i++) {
|
|
1135
|
+
pushParam(t.segments.slice(0, i).join("."));
|
|
1136
|
+
}
|
|
1137
|
+
pushSelector(`expand.${t.segments[0]}`);
|
|
1138
|
+
} else {
|
|
1139
|
+
const depth = Math.max(1, t.depth);
|
|
1140
|
+
const relationPath = t.segments.slice(0, depth).join(".");
|
|
1141
|
+
pushParam(relationPath);
|
|
1142
|
+
pushSelector(`expand.${t.segments.slice(0, depth + 1).join(".")}`);
|
|
1143
|
+
if (t.segments.length === depth + 1) {
|
|
1144
|
+
const base = t.segments[0];
|
|
1145
|
+
const leaf = t.segments[depth];
|
|
1146
|
+
const leaves = narrow.get(base) ?? [];
|
|
1147
|
+
if (!leaves.includes(leaf)) leaves.push(leaf);
|
|
1148
|
+
narrow.set(base, leaves);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
return {
|
|
1153
|
+
expand: expandParams.length > 0 ? expandParams.join(",") : void 0,
|
|
1154
|
+
fieldSelectors,
|
|
1155
|
+
narrow
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* Keep only the requested leaf fields on `record.expand.<relation>`.
|
|
1160
|
+
*
|
|
1161
|
+
* Mirrors PocketBase's server-side `fields=expand.<rel>.<field>` projection
|
|
1162
|
+
* for servers that don't support it (the LazyPock server), so field
|
|
1163
|
+
* selection behaves the same against both. A no-op when the server already
|
|
1164
|
+
* narrowed the record. Multi-relations are narrowed element-wise.
|
|
1165
|
+
*/
|
|
1166
|
+
applyExpandNarrowing(record, narrow) {
|
|
1167
|
+
if (narrow.size === 0) return record;
|
|
1168
|
+
const rec = record;
|
|
1169
|
+
const expand = rec.expand;
|
|
1170
|
+
if (!expand || typeof expand !== "object") return record;
|
|
1171
|
+
const next = { ...expand };
|
|
1172
|
+
for (const [relation, leaves] of narrow) {
|
|
1173
|
+
const value = next[relation];
|
|
1174
|
+
if (value === void 0 || value === null) continue;
|
|
1175
|
+
next[relation] = Array.isArray(value) ? value.map((el) => pickFields(el, leaves)) : pickFields(value, leaves);
|
|
1176
|
+
}
|
|
1177
|
+
return { ...rec, expand: next };
|
|
1178
|
+
}
|
|
1179
|
+
/**
|
|
1180
|
+
* Merge the expand-preserving selectors into the base field projection.
|
|
1181
|
+
*
|
|
1182
|
+
* Both servers are kept happy at once:
|
|
1183
|
+
* - PocketBase drops the whole `expand` payload unless an `expand.<key>`
|
|
1184
|
+
* entry is present in `fields`;
|
|
1185
|
+
* - the LazyPock server projects first (strict, top-level only) and then
|
|
1186
|
+
* expands, so the relation *field* itself must be present in `fields`.
|
|
1187
|
+
*
|
|
1188
|
+
* We therefore add both the relation field (`cate`) and the
|
|
1189
|
+
* `expand.<key>` entry. When no projection is active, no `fields` param is
|
|
1190
|
+
* sent at all — a value like `*` plus other tokens is not a wildcard on
|
|
1191
|
+
* the LazyPock server (it is a strict take, yielding empty records) and
|
|
1192
|
+
* the server keeps `expand` by default when `fields` is absent.
|
|
1193
|
+
*/
|
|
1194
|
+
mergeExpandFields(base, info) {
|
|
1195
|
+
if (info.fieldSelectors.length === 0) return base;
|
|
1196
|
+
if (base === void 0) return void 0;
|
|
1197
|
+
const parts = base.split(",").map((f) => f.trim()).filter(Boolean);
|
|
1198
|
+
const seen = new Set(parts);
|
|
1199
|
+
const push = (value) => {
|
|
1200
|
+
if (seen.has(value)) return;
|
|
1201
|
+
seen.add(value);
|
|
1202
|
+
parts.push(value);
|
|
1203
|
+
};
|
|
1204
|
+
const fullKeys = /* @__PURE__ */ new Set();
|
|
1205
|
+
for (const selector of info.fieldSelectors) {
|
|
1206
|
+
const seg = selector.split(".");
|
|
1207
|
+
if (seg.length === 2) fullKeys.add(seg[1]);
|
|
1208
|
+
}
|
|
1209
|
+
for (const selector of info.fieldSelectors) {
|
|
1210
|
+
const seg = selector.split(".");
|
|
1211
|
+
if (seg.length > 2 && fullKeys.has(seg[1])) continue;
|
|
1212
|
+
if (seg.length >= 2) push(seg[1]);
|
|
1213
|
+
push(selector);
|
|
1214
|
+
}
|
|
1215
|
+
return parts.join(",");
|
|
629
1216
|
}
|
|
630
1217
|
/**
|
|
631
1218
|
* Fetch a paginated list of records (PocketBase `getList`).
|
|
@@ -634,7 +1221,7 @@ var CollectionService = class _CollectionService {
|
|
|
634
1221
|
* @param perPage Records per page (default 30).
|
|
635
1222
|
* @param options Query params (`filter`, `sort`, `expand`, `fields`) + request options.
|
|
636
1223
|
*/
|
|
637
|
-
getList(page = 1, perPage = 30, options) {
|
|
1224
|
+
async getList(page = 1, perPage = 30, options) {
|
|
638
1225
|
const {
|
|
639
1226
|
requestKey,
|
|
640
1227
|
autoCancel,
|
|
@@ -646,12 +1233,24 @@ var CollectionService = class _CollectionService {
|
|
|
646
1233
|
params,
|
|
647
1234
|
...queryParams
|
|
648
1235
|
} = options ?? {};
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
1236
|
+
if (Array.isArray(queryParams.sort)) {
|
|
1237
|
+
queryParams.sort = queryParams.sort.join(",");
|
|
1238
|
+
}
|
|
1239
|
+
if (Array.isArray(queryParams.expand)) {
|
|
1240
|
+
queryParams.expand = queryParams.expand.join(",");
|
|
652
1241
|
}
|
|
653
|
-
|
|
654
|
-
|
|
1242
|
+
const expandInfo = this.normalizeExpand(
|
|
1243
|
+
typeof queryParams.expand === "string" ? queryParams.expand : void 0
|
|
1244
|
+
);
|
|
1245
|
+
if (expandInfo.expand !== void 0) {
|
|
1246
|
+
queryParams.expand = expandInfo.expand;
|
|
1247
|
+
}
|
|
1248
|
+
const fields = this.mergeExpandFields(
|
|
1249
|
+
this.effectiveFields(queryParams.fields),
|
|
1250
|
+
expandInfo
|
|
1251
|
+
);
|
|
1252
|
+
if (fields !== void 0) {
|
|
1253
|
+
queryParams.fields = fields;
|
|
655
1254
|
}
|
|
656
1255
|
const qs = new URLSearchParams(
|
|
657
1256
|
Object.fromEntries(
|
|
@@ -662,19 +1261,22 @@ var CollectionService = class _CollectionService {
|
|
|
662
1261
|
}).map(([k, v]) => [k, String(v)])
|
|
663
1262
|
)
|
|
664
1263
|
).toString();
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
1264
|
+
const result = await this.http.get("/" + this.encodeId(this.collectionName) + "?" + qs, {
|
|
1265
|
+
requestKey,
|
|
1266
|
+
autoCancel,
|
|
1267
|
+
cancelKey,
|
|
1268
|
+
fetch,
|
|
1269
|
+
headers,
|
|
1270
|
+
signal,
|
|
1271
|
+
singleFlight,
|
|
1272
|
+
params
|
|
1273
|
+
});
|
|
1274
|
+
if (result && expandInfo.narrow.size > 0) {
|
|
1275
|
+
result.items = result.items.map(
|
|
1276
|
+
(item) => this.applyExpandNarrowing(item, expandInfo.narrow)
|
|
1277
|
+
);
|
|
1278
|
+
}
|
|
1279
|
+
return result;
|
|
678
1280
|
}
|
|
679
1281
|
/**
|
|
680
1282
|
* Fetch all records at once (auto-paginates). Mirrors PocketBase's
|
|
@@ -722,19 +1324,32 @@ var CollectionService = class _CollectionService {
|
|
|
722
1324
|
* @param id Record ID.
|
|
723
1325
|
* @param options Optional request options.
|
|
724
1326
|
*/
|
|
725
|
-
getOne(id, options) {
|
|
726
|
-
|
|
727
|
-
if (
|
|
728
|
-
|
|
729
|
-
}
|
|
1327
|
+
async getOne(id, options) {
|
|
1328
|
+
let rawExpand;
|
|
1329
|
+
if (Array.isArray(options?.expand)) {
|
|
1330
|
+
rawExpand = options.expand.join(",");
|
|
1331
|
+
} else if (typeof options?.expand === "string") {
|
|
1332
|
+
rawExpand = options.expand;
|
|
1333
|
+
}
|
|
1334
|
+
const expandInfo = this.normalizeExpand(rawExpand);
|
|
1335
|
+
const fields = this.mergeExpandFields(
|
|
1336
|
+
this.effectiveFields(options?.fields),
|
|
1337
|
+
expandInfo
|
|
1338
|
+
);
|
|
730
1339
|
const qs = new URLSearchParams();
|
|
731
1340
|
if (fields !== void 0) qs.set("fields", fields);
|
|
732
|
-
if (
|
|
1341
|
+
if (rawExpand !== void 0) {
|
|
1342
|
+
qs.set("expand", expandInfo.expand ?? rawExpand);
|
|
1343
|
+
}
|
|
733
1344
|
const qsStr = qs.toString();
|
|
734
|
-
|
|
1345
|
+
const record = await this.http.get(
|
|
735
1346
|
"/" + this.encodeId(this.collectionName) + "/" + this.encodeId(id) + (qsStr ? "?" + qsStr : ""),
|
|
736
1347
|
options
|
|
737
1348
|
);
|
|
1349
|
+
if (record && expandInfo.narrow.size > 0) {
|
|
1350
|
+
return this.applyExpandNarrowing(record, expandInfo.narrow);
|
|
1351
|
+
}
|
|
1352
|
+
return record;
|
|
738
1353
|
}
|
|
739
1354
|
/**
|
|
740
1355
|
* Create a new record.
|
|
@@ -818,7 +1433,8 @@ var CollectionService = class _CollectionService {
|
|
|
818
1433
|
this.collectionName,
|
|
819
1434
|
this.authStore,
|
|
820
1435
|
this.realtime,
|
|
821
|
-
schema
|
|
1436
|
+
schema,
|
|
1437
|
+
this.schemaResolver
|
|
822
1438
|
);
|
|
823
1439
|
derived.fieldsPreset = this.fieldsPreset;
|
|
824
1440
|
return derived;
|
|
@@ -904,15 +1520,108 @@ var CollectionService = class _CollectionService {
|
|
|
904
1520
|
return data;
|
|
905
1521
|
}
|
|
906
1522
|
/**
|
|
907
|
-
* Get available auth methods for this collection.
|
|
1523
|
+
* Get available auth methods for this collection (PocketBase `listAuthMethods`).
|
|
1524
|
+
* Returns `password`, `oauth2.providers` (each with `name`, `authURL`,
|
|
1525
|
+
* `state`, `codeVerifier`), and `mfa`.
|
|
908
1526
|
*/
|
|
909
|
-
|
|
910
|
-
async authMethods(options) {
|
|
1527
|
+
async listAuthMethods(options) {
|
|
911
1528
|
return this.http.get(
|
|
912
1529
|
"/" + this.encodeId(this.collectionName) + "/auth-methods",
|
|
913
1530
|
options
|
|
914
1531
|
);
|
|
915
1532
|
}
|
|
1533
|
+
/**
|
|
1534
|
+
* Get available auth methods for this collection.
|
|
1535
|
+
* @deprecated Use {@link listAuthMethods} (typed).
|
|
1536
|
+
*/
|
|
1537
|
+
authMethods(options) {
|
|
1538
|
+
return this.listAuthMethods(options);
|
|
1539
|
+
}
|
|
1540
|
+
/**
|
|
1541
|
+
* Sign in with an OAuth2 provider (PocketBase `authWithOAuth2` parity).
|
|
1542
|
+
*
|
|
1543
|
+
* One call handles the whole web flow: fetch the provider's `authURL`,
|
|
1544
|
+
* open a popup (or call `options.urlCallback`), wait for the backend's
|
|
1545
|
+
* `postMessage` result, populate the auth store, and resolve with
|
|
1546
|
+
* `{ token, record, meta }`.
|
|
1547
|
+
*
|
|
1548
|
+
* `options.createData` is accepted for PocketBase signature parity but is
|
|
1549
|
+
* **not** forwarded by the backend's popup redirect flow — use
|
|
1550
|
+
* {@link authWithOAuth2Code} when you need extra fields on first sign-up.
|
|
1551
|
+
*
|
|
1552
|
+
* React Native / non-browser: the popup flow depends on
|
|
1553
|
+
* `window.postMessage`. Use {@link listAuthMethods} + {@link authWithOAuth2Code}
|
|
1554
|
+
* instead.
|
|
1555
|
+
*/
|
|
1556
|
+
async authWithOAuth2(options) {
|
|
1557
|
+
const {
|
|
1558
|
+
provider,
|
|
1559
|
+
urlCallback,
|
|
1560
|
+
popup,
|
|
1561
|
+
timeoutMs = DEFAULT_OAUTH2_TIMEOUT_MS
|
|
1562
|
+
} = options;
|
|
1563
|
+
const methods = await this.listAuthMethods();
|
|
1564
|
+
const providerInfo = methods?.oauth2?.providers?.find(
|
|
1565
|
+
(p) => p.name === provider
|
|
1566
|
+
);
|
|
1567
|
+
if (!providerInfo) {
|
|
1568
|
+
throw new ApiError(
|
|
1569
|
+
`OAuth2 provider "${provider}" is not enabled for this collection.`,
|
|
1570
|
+
methods ?? {},
|
|
1571
|
+
400,
|
|
1572
|
+
false,
|
|
1573
|
+
"oauth2_unknown_provider"
|
|
1574
|
+
);
|
|
1575
|
+
}
|
|
1576
|
+
const result = await authorizeWithOAuth2Popup({
|
|
1577
|
+
authURL: providerInfo.authURL,
|
|
1578
|
+
expectedOrigin: oauth2RedirectOrigin(this.http.baseUrl),
|
|
1579
|
+
urlCallback,
|
|
1580
|
+
popup,
|
|
1581
|
+
timeoutMs
|
|
1582
|
+
});
|
|
1583
|
+
if (this.authStore) {
|
|
1584
|
+
this.authStore.setCollectionName(this.collectionName);
|
|
1585
|
+
this.authStore.set(result.token, result.record);
|
|
1586
|
+
}
|
|
1587
|
+
return result;
|
|
1588
|
+
}
|
|
1589
|
+
/**
|
|
1590
|
+
* Exchange an OAuth2 authorization code directly (PocketBase
|
|
1591
|
+
* `authWithOAuth2Code` parity).
|
|
1592
|
+
*
|
|
1593
|
+
* Used by mobile / non-browser flows: fetch {@link listAuthMethods} to get
|
|
1594
|
+
* the provider `authURL` + `codeVerifier`, present the URL yourself,
|
|
1595
|
+
* capture the `code` from your redirect/deep link, then call this to
|
|
1596
|
+
* finish and populate the auth store.
|
|
1597
|
+
*/
|
|
1598
|
+
async authWithOAuth2Code(options) {
|
|
1599
|
+
const { provider, code, codeVerifier, redirectUrl, createData } = options;
|
|
1600
|
+
const data = await this.http.post(
|
|
1601
|
+
"/" + this.encodeId(this.collectionName) + "/auth-with-oauth2",
|
|
1602
|
+
{
|
|
1603
|
+
provider,
|
|
1604
|
+
code,
|
|
1605
|
+
codeVerifier,
|
|
1606
|
+
redirectUrl: redirectUrl ?? this.http.baseUrl + "/oauth2-redirect",
|
|
1607
|
+
createData: createData ?? {}
|
|
1608
|
+
}
|
|
1609
|
+
);
|
|
1610
|
+
if (!data) {
|
|
1611
|
+
throw new ApiError(
|
|
1612
|
+
"OAuth2 code exchange returned no data.",
|
|
1613
|
+
{},
|
|
1614
|
+
0,
|
|
1615
|
+
false,
|
|
1616
|
+
"oauth2_exchange_failed"
|
|
1617
|
+
);
|
|
1618
|
+
}
|
|
1619
|
+
if (this.authStore) {
|
|
1620
|
+
this.authStore.setCollectionName(this.collectionName);
|
|
1621
|
+
this.authStore.set(data.token, data.record);
|
|
1622
|
+
}
|
|
1623
|
+
return data;
|
|
1624
|
+
}
|
|
916
1625
|
};
|
|
917
1626
|
|
|
918
1627
|
// src/realtime.ts
|
|
@@ -1803,6 +2512,10 @@ var LazypockClient = class {
|
|
|
1803
2512
|
this.schemaByName = new Map(
|
|
1804
2513
|
options.types.schemas.map((s) => [s.name, s])
|
|
1805
2514
|
);
|
|
2515
|
+
this.schemaById = /* @__PURE__ */ new Map();
|
|
2516
|
+
for (const s of options.types.schemas) {
|
|
2517
|
+
if (s.id) this.schemaById.set(String(s.id), s);
|
|
2518
|
+
}
|
|
1806
2519
|
}
|
|
1807
2520
|
}
|
|
1808
2521
|
/**
|
|
@@ -1825,7 +2538,10 @@ var LazypockClient = class {
|
|
|
1825
2538
|
this.authStore,
|
|
1826
2539
|
this.realtime,
|
|
1827
2540
|
// Prefer the explicit schema, then the client-level `types.schemas`.
|
|
1828
|
-
schema ?? this.schemaByName?.get(name)
|
|
2541
|
+
schema ?? this.schemaByName?.get(name),
|
|
2542
|
+
// Let the service follow relation fields into other collections
|
|
2543
|
+
// (expand field selection vs nested-relation expansion).
|
|
2544
|
+
(key) => this.schemaByName?.get(key) ?? this.schemaById?.get(key)
|
|
1829
2545
|
);
|
|
1830
2546
|
this.collectionCache.set(name, svc);
|
|
1831
2547
|
}
|
|
@@ -2021,6 +2737,8 @@ function createClient(options) {
|
|
|
2021
2737
|
CollectionService,
|
|
2022
2738
|
CollectionsService,
|
|
2023
2739
|
FilesService,
|
|
2740
|
+
FilterBuilder,
|
|
2741
|
+
FilterExpr,
|
|
2024
2742
|
HttpClient,
|
|
2025
2743
|
LazypockClient,
|
|
2026
2744
|
RealtimeService,
|