lazypock 0.13.0 → 0.15.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 +139 -60
- package/dist/index.cjs +483 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +235 -14
- package/dist/index.d.ts +235 -14
- package/dist/index.global.js +481 -40
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +481 -40
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/collection.ts +450 -41
- package/src/filter.ts +193 -0
- package/src/index.ts +5 -0
- package/src/lazypock.ts +16 -0
- package/src/types.ts +59 -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,
|
|
@@ -475,6 +477,144 @@ var memoryStorage = {
|
|
|
475
477
|
}
|
|
476
478
|
};
|
|
477
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
|
+
|
|
478
618
|
// src/oauth2.ts
|
|
479
619
|
var DEFAULT_POPUP_WIDTH = 500;
|
|
480
620
|
var DEFAULT_POPUP_HEIGHT = 700;
|
|
@@ -708,14 +848,24 @@ function normalizeAction(event, rawAction) {
|
|
|
708
848
|
if (event === "delete" || event === "record_delete") return "delete";
|
|
709
849
|
return "update";
|
|
710
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
|
+
}
|
|
711
860
|
var CollectionService = class _CollectionService {
|
|
712
861
|
/** @internal */
|
|
713
|
-
constructor(http, collectionName, authStore, realtime, schema) {
|
|
862
|
+
constructor(http, collectionName, authStore, realtime, schema, schemaResolver) {
|
|
714
863
|
this.http = http;
|
|
715
864
|
this.collectionName = collectionName;
|
|
716
865
|
this.authStore = authStore;
|
|
717
866
|
this.realtime = realtime;
|
|
718
867
|
this.schema = schema;
|
|
868
|
+
this.schemaResolver = schemaResolver;
|
|
719
869
|
}
|
|
720
870
|
encodeId(id) {
|
|
721
871
|
return encodeURIComponent(id);
|
|
@@ -750,7 +900,8 @@ var CollectionService = class _CollectionService {
|
|
|
750
900
|
this.collectionName,
|
|
751
901
|
this.authStore,
|
|
752
902
|
this.realtime,
|
|
753
|
-
this.schema
|
|
903
|
+
this.schema,
|
|
904
|
+
this.schemaResolver
|
|
754
905
|
);
|
|
755
906
|
derived.fieldsPreset = preset;
|
|
756
907
|
if (this.schema && preset !== "*") {
|
|
@@ -813,20 +964,266 @@ var CollectionService = class _CollectionService {
|
|
|
813
964
|
])
|
|
814
965
|
].join(",");
|
|
815
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 a `filter` callback into a concrete string / {@link FilterExpr}.
|
|
995
|
+
* The callback receives this collection's typed `where` helper, so
|
|
996
|
+
* `filter: (w) => w("title").contains("x")` type-checks field names inline.
|
|
997
|
+
*/
|
|
998
|
+
resolveFilter(filter) {
|
|
999
|
+
if (typeof filter !== "function") {
|
|
1000
|
+
return filter;
|
|
1001
|
+
}
|
|
1002
|
+
return filter((field) => this.where(field));
|
|
1003
|
+
}
|
|
1004
|
+
/**
|
|
1005
|
+
* Resolve the leading relation chain of an expand token against the
|
|
1006
|
+
* (optionally schema-aware) collection graph.
|
|
1007
|
+
*
|
|
1008
|
+
* Walks `segments` from this collection: each segment that is a relation
|
|
1009
|
+
* advances to its target collection. Stops at the first non-relation
|
|
1010
|
+
* segment or when the target schema is unknown.
|
|
1011
|
+
*
|
|
1012
|
+
* @returns `depth` — how many leading segments are relations; and
|
|
1013
|
+
* `canInspect` — whether the schema at `depth` is known (so the next
|
|
1014
|
+
* segment can be classified as a field selection vs a nested relation).
|
|
1015
|
+
*/
|
|
1016
|
+
resolveRelationChain(segments) {
|
|
1017
|
+
let currentFields = this.schema?.fields;
|
|
1018
|
+
if (!currentFields) return { depth: 0, canInspect: false };
|
|
1019
|
+
for (let i = 0; i < segments.length; i++) {
|
|
1020
|
+
const field = currentFields.find((f) => f.name === segments[i]);
|
|
1021
|
+
if (!field || field.type !== "relation") {
|
|
1022
|
+
return { depth: i, canInspect: true };
|
|
1023
|
+
}
|
|
1024
|
+
if (i === segments.length - 1) {
|
|
1025
|
+
return { depth: segments.length, canInspect: true };
|
|
1026
|
+
}
|
|
1027
|
+
const target = this.resolveTargetCollection(field);
|
|
1028
|
+
if (!target?.fields) {
|
|
1029
|
+
return { depth: i + 1, canInspect: false };
|
|
1030
|
+
}
|
|
1031
|
+
currentFields = target.fields;
|
|
1032
|
+
}
|
|
1033
|
+
return { depth: segments.length, canInspect: true };
|
|
1034
|
+
}
|
|
1035
|
+
/** Follow a relation field to its target collection, if resolvable. */
|
|
1036
|
+
resolveTargetCollection(field) {
|
|
1037
|
+
const key = field.options?.collection ?? field.options?.collectionId ?? field.collectionId ?? void 0;
|
|
1038
|
+
if (!key || !this.schemaResolver) return void 0;
|
|
1039
|
+
return this.schemaResolver(String(key));
|
|
1040
|
+
}
|
|
816
1041
|
/** Warn once when an expand field is not a relation (schema known). */
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
this.schema.fields.filter((f) => f.type === "relation").map((f) => f.name)
|
|
1042
|
+
warnNonRelation(name) {
|
|
1043
|
+
console.warn(
|
|
1044
|
+
`[lazypock] expand("${name}"): field is not a relation on collection "${this.collectionName}"`
|
|
821
1045
|
);
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
1046
|
+
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Validate and normalise a caller's `expand` string.
|
|
1049
|
+
*
|
|
1050
|
+
* PocketBase only expands *relations*: a dotted token like `author.user`
|
|
1051
|
+
* walks into the related record. Callers often use a dotted token to mean
|
|
1052
|
+
* "only these fields of the expanded record" (`owner.name,owner.email`),
|
|
1053
|
+
* which PocketBase silently drops. When the trailing segment is not a
|
|
1054
|
+
* relation — resolved from the schema graph — the token is rewritten as a
|
|
1055
|
+
* field selection:
|
|
1056
|
+
*
|
|
1057
|
+
* ```
|
|
1058
|
+
* expand: 'owner.name,owner.email'
|
|
1059
|
+
* → expand=owner&fields=<base>,expand.owner.name,expand.owner.email
|
|
1060
|
+
* ```
|
|
1061
|
+
*
|
|
1062
|
+
* Tokens whose whole path is relations keep their existing nested-expand
|
|
1063
|
+
* behaviour. Without a schema the two are indistinguishable, so a lone
|
|
1064
|
+
* dotted token stays a nested expand (with a warning) while two or more
|
|
1065
|
+
* dotted tokens sharing the same relation are treated as field selection.
|
|
1066
|
+
*/
|
|
1067
|
+
normalizeExpand(expand) {
|
|
1068
|
+
const narrow = /* @__PURE__ */ new Map();
|
|
1069
|
+
if (!expand) return { fieldSelectors: [], narrow };
|
|
1070
|
+
const tokens = expand.split(",").map((t) => t.trim()).filter(Boolean);
|
|
1071
|
+
if (tokens.length === 0) return { fieldSelectors: [], narrow };
|
|
1072
|
+
const relations = this.schema?.fields ? new Set(
|
|
1073
|
+
this.schema.fields.filter((f) => f.type === "relation").map((f) => f.name)
|
|
1074
|
+
) : void 0;
|
|
1075
|
+
const parsed = [];
|
|
1076
|
+
for (const token of tokens) {
|
|
1077
|
+
const segments = token.split(".").filter(Boolean);
|
|
1078
|
+
const base = segments[0] ?? token;
|
|
1079
|
+
if (segments.length <= 1) {
|
|
1080
|
+
if (relations && !relations.has(base)) {
|
|
1081
|
+
this.warnNonRelation(base);
|
|
1082
|
+
}
|
|
1083
|
+
parsed.push({ token, base, segments, kind: "plain", depth: 0 });
|
|
1084
|
+
continue;
|
|
1085
|
+
}
|
|
1086
|
+
if (!relations) {
|
|
1087
|
+
parsed.push({ token, base, segments, kind: "unknown", depth: 1 });
|
|
1088
|
+
continue;
|
|
1089
|
+
}
|
|
1090
|
+
const { depth, canInspect } = this.resolveRelationChain(segments);
|
|
1091
|
+
if (depth === 0) {
|
|
1092
|
+
this.warnNonRelation(base);
|
|
1093
|
+
parsed.push({ token, base, segments, kind: "plain", depth: 0 });
|
|
1094
|
+
} else if (depth === segments.length) {
|
|
1095
|
+
parsed.push({ token, base, segments, kind: "nested", depth });
|
|
1096
|
+
} else if (canInspect) {
|
|
1097
|
+
parsed.push({ token, base, segments, kind: "narrow", depth });
|
|
1098
|
+
} else {
|
|
1099
|
+
parsed.push({ token, base, segments, kind: "unknown", depth });
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
const byBase = /* @__PURE__ */ new Map();
|
|
1103
|
+
for (const t of parsed) {
|
|
1104
|
+
const arr = byBase.get(t.base);
|
|
1105
|
+
if (arr) arr.push(t);
|
|
1106
|
+
else byBase.set(t.base, [t]);
|
|
1107
|
+
}
|
|
1108
|
+
for (const [base, group] of byBase) {
|
|
1109
|
+
const unknowns = group.filter((t) => t.kind === "unknown");
|
|
1110
|
+
if (unknowns.length === 0) continue;
|
|
1111
|
+
const distinct = new Set(unknowns.map((t) => t.token));
|
|
1112
|
+
const hasNarrow = group.some((t) => t.kind === "narrow");
|
|
1113
|
+
if (!hasNarrow && distinct.size < 2) {
|
|
1114
|
+
for (const t of unknowns) {
|
|
1115
|
+
t.kind = "nested";
|
|
1116
|
+
console.warn(
|
|
1117
|
+
`[lazypock] expand("${t.token}"): treated as a nested relation. Pass types.schemas (or use fields=) to select fields of "${base}".`
|
|
1118
|
+
);
|
|
1119
|
+
}
|
|
1120
|
+
} else {
|
|
1121
|
+
for (const t of unknowns) t.kind = "narrow";
|
|
828
1122
|
}
|
|
829
1123
|
}
|
|
1124
|
+
const expandParams = [];
|
|
1125
|
+
const expandSeen = /* @__PURE__ */ new Set();
|
|
1126
|
+
const fieldSelectors = [];
|
|
1127
|
+
const selectorSeen = /* @__PURE__ */ new Set();
|
|
1128
|
+
const pushParam = (p) => {
|
|
1129
|
+
if (!expandSeen.has(p)) {
|
|
1130
|
+
expandSeen.add(p);
|
|
1131
|
+
expandParams.push(p);
|
|
1132
|
+
}
|
|
1133
|
+
};
|
|
1134
|
+
const pushSelector = (s) => {
|
|
1135
|
+
if (!selectorSeen.has(s)) {
|
|
1136
|
+
selectorSeen.add(s);
|
|
1137
|
+
fieldSelectors.push(s);
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
1140
|
+
for (const t of parsed) {
|
|
1141
|
+
if (t.kind === "plain") {
|
|
1142
|
+
pushParam(t.token);
|
|
1143
|
+
pushSelector(`expand.${t.token}`);
|
|
1144
|
+
} else if (t.kind === "nested") {
|
|
1145
|
+
for (let i = 1; i <= t.segments.length; i++) {
|
|
1146
|
+
pushParam(t.segments.slice(0, i).join("."));
|
|
1147
|
+
}
|
|
1148
|
+
pushSelector(`expand.${t.segments[0]}`);
|
|
1149
|
+
} else {
|
|
1150
|
+
const depth = Math.max(1, t.depth);
|
|
1151
|
+
const relationPath = t.segments.slice(0, depth).join(".");
|
|
1152
|
+
pushParam(relationPath);
|
|
1153
|
+
pushSelector(`expand.${t.segments.slice(0, depth + 1).join(".")}`);
|
|
1154
|
+
if (t.segments.length === depth + 1) {
|
|
1155
|
+
const base = t.segments[0];
|
|
1156
|
+
const leaf = t.segments[depth];
|
|
1157
|
+
const leaves = narrow.get(base) ?? [];
|
|
1158
|
+
if (!leaves.includes(leaf)) leaves.push(leaf);
|
|
1159
|
+
narrow.set(base, leaves);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
return {
|
|
1164
|
+
expand: expandParams.length > 0 ? expandParams.join(",") : void 0,
|
|
1165
|
+
fieldSelectors,
|
|
1166
|
+
narrow
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
/**
|
|
1170
|
+
* Keep only the requested leaf fields on `record.expand.<relation>`.
|
|
1171
|
+
*
|
|
1172
|
+
* Mirrors PocketBase's server-side `fields=expand.<rel>.<field>` projection
|
|
1173
|
+
* for servers that don't support it (the LazyPock server), so field
|
|
1174
|
+
* selection behaves the same against both. A no-op when the server already
|
|
1175
|
+
* narrowed the record. Multi-relations are narrowed element-wise.
|
|
1176
|
+
*/
|
|
1177
|
+
applyExpandNarrowing(record, narrow) {
|
|
1178
|
+
if (narrow.size === 0) return record;
|
|
1179
|
+
const rec = record;
|
|
1180
|
+
const expand = rec.expand;
|
|
1181
|
+
if (!expand || typeof expand !== "object") return record;
|
|
1182
|
+
const next = { ...expand };
|
|
1183
|
+
for (const [relation, leaves] of narrow) {
|
|
1184
|
+
const value = next[relation];
|
|
1185
|
+
if (value === void 0 || value === null) continue;
|
|
1186
|
+
next[relation] = Array.isArray(value) ? value.map((el) => pickFields(el, leaves)) : pickFields(value, leaves);
|
|
1187
|
+
}
|
|
1188
|
+
return { ...rec, expand: next };
|
|
1189
|
+
}
|
|
1190
|
+
/**
|
|
1191
|
+
* Merge the expand-preserving selectors into the base field projection.
|
|
1192
|
+
*
|
|
1193
|
+
* Both servers are kept happy at once:
|
|
1194
|
+
* - PocketBase drops the whole `expand` payload unless an `expand.<key>`
|
|
1195
|
+
* entry is present in `fields`;
|
|
1196
|
+
* - the LazyPock server projects first (strict, top-level only) and then
|
|
1197
|
+
* expands, so the relation *field* itself must be present in `fields`.
|
|
1198
|
+
*
|
|
1199
|
+
* We therefore add both the relation field (`cate`) and the
|
|
1200
|
+
* `expand.<key>` entry. When no projection is active, no `fields` param is
|
|
1201
|
+
* sent at all — a value like `*` plus other tokens is not a wildcard on
|
|
1202
|
+
* the LazyPock server (it is a strict take, yielding empty records) and
|
|
1203
|
+
* the server keeps `expand` by default when `fields` is absent.
|
|
1204
|
+
*/
|
|
1205
|
+
mergeExpandFields(base, info) {
|
|
1206
|
+
if (info.fieldSelectors.length === 0) return base;
|
|
1207
|
+
if (base === void 0) return void 0;
|
|
1208
|
+
const parts = base.split(",").map((f) => f.trim()).filter(Boolean);
|
|
1209
|
+
const seen = new Set(parts);
|
|
1210
|
+
const push = (value) => {
|
|
1211
|
+
if (seen.has(value)) return;
|
|
1212
|
+
seen.add(value);
|
|
1213
|
+
parts.push(value);
|
|
1214
|
+
};
|
|
1215
|
+
const fullKeys = /* @__PURE__ */ new Set();
|
|
1216
|
+
for (const selector of info.fieldSelectors) {
|
|
1217
|
+
const seg = selector.split(".");
|
|
1218
|
+
if (seg.length === 2) fullKeys.add(seg[1]);
|
|
1219
|
+
}
|
|
1220
|
+
for (const selector of info.fieldSelectors) {
|
|
1221
|
+
const seg = selector.split(".");
|
|
1222
|
+
if (seg.length > 2 && fullKeys.has(seg[1])) continue;
|
|
1223
|
+
if (seg.length >= 2) push(seg[1]);
|
|
1224
|
+
push(selector);
|
|
1225
|
+
}
|
|
1226
|
+
return parts.join(",");
|
|
830
1227
|
}
|
|
831
1228
|
/**
|
|
832
1229
|
* Fetch a paginated list of records (PocketBase `getList`).
|
|
@@ -835,7 +1232,7 @@ var CollectionService = class _CollectionService {
|
|
|
835
1232
|
* @param perPage Records per page (default 30).
|
|
836
1233
|
* @param options Query params (`filter`, `sort`, `expand`, `fields`) + request options.
|
|
837
1234
|
*/
|
|
838
|
-
getList(page = 1, perPage = 30, options) {
|
|
1235
|
+
async getList(page = 1, perPage = 30, options) {
|
|
839
1236
|
const {
|
|
840
1237
|
requestKey,
|
|
841
1238
|
autoCancel,
|
|
@@ -847,12 +1244,29 @@ var CollectionService = class _CollectionService {
|
|
|
847
1244
|
params,
|
|
848
1245
|
...queryParams
|
|
849
1246
|
} = options ?? {};
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
queryParams.fields = fields;
|
|
1247
|
+
if (Array.isArray(queryParams.sort)) {
|
|
1248
|
+
queryParams.sort = queryParams.sort.join(",");
|
|
853
1249
|
}
|
|
854
|
-
if (
|
|
855
|
-
|
|
1250
|
+
if (Array.isArray(queryParams.expand)) {
|
|
1251
|
+
queryParams.expand = queryParams.expand.join(",");
|
|
1252
|
+
}
|
|
1253
|
+
if (typeof queryParams.filter === "function") {
|
|
1254
|
+
queryParams.filter = this.resolveFilter(
|
|
1255
|
+
queryParams.filter
|
|
1256
|
+
);
|
|
1257
|
+
}
|
|
1258
|
+
const expandInfo = this.normalizeExpand(
|
|
1259
|
+
typeof queryParams.expand === "string" ? queryParams.expand : void 0
|
|
1260
|
+
);
|
|
1261
|
+
if (expandInfo.expand !== void 0) {
|
|
1262
|
+
queryParams.expand = expandInfo.expand;
|
|
1263
|
+
}
|
|
1264
|
+
const fields = this.mergeExpandFields(
|
|
1265
|
+
this.effectiveFields(queryParams.fields),
|
|
1266
|
+
expandInfo
|
|
1267
|
+
);
|
|
1268
|
+
if (fields !== void 0) {
|
|
1269
|
+
queryParams.fields = fields;
|
|
856
1270
|
}
|
|
857
1271
|
const qs = new URLSearchParams(
|
|
858
1272
|
Object.fromEntries(
|
|
@@ -863,19 +1277,22 @@ var CollectionService = class _CollectionService {
|
|
|
863
1277
|
}).map(([k, v]) => [k, String(v)])
|
|
864
1278
|
)
|
|
865
1279
|
).toString();
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
1280
|
+
const result = await this.http.get("/" + this.encodeId(this.collectionName) + "?" + qs, {
|
|
1281
|
+
requestKey,
|
|
1282
|
+
autoCancel,
|
|
1283
|
+
cancelKey,
|
|
1284
|
+
fetch,
|
|
1285
|
+
headers,
|
|
1286
|
+
signal,
|
|
1287
|
+
singleFlight,
|
|
1288
|
+
params
|
|
1289
|
+
});
|
|
1290
|
+
if (result && expandInfo.narrow.size > 0) {
|
|
1291
|
+
result.items = result.items.map(
|
|
1292
|
+
(item) => this.applyExpandNarrowing(item, expandInfo.narrow)
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
1295
|
+
return result;
|
|
879
1296
|
}
|
|
880
1297
|
/**
|
|
881
1298
|
* Fetch all records at once (auto-paginates). Mirrors PocketBase's
|
|
@@ -885,6 +1302,9 @@ var CollectionService = class _CollectionService {
|
|
|
885
1302
|
*/
|
|
886
1303
|
async getFullList(options) {
|
|
887
1304
|
const { batch = 1e3, ...rest } = options ?? {};
|
|
1305
|
+
if (typeof rest.filter === "function") {
|
|
1306
|
+
rest.filter = this.resolveFilter(rest.filter);
|
|
1307
|
+
}
|
|
888
1308
|
const effectiveKey = typeof rest.requestKey === "string" ? rest.requestKey : `getFullList:${this.collectionName}:${stableStringify(rest)}`;
|
|
889
1309
|
const items = [];
|
|
890
1310
|
let page = 1;
|
|
@@ -923,19 +1343,32 @@ var CollectionService = class _CollectionService {
|
|
|
923
1343
|
* @param id Record ID.
|
|
924
1344
|
* @param options Optional request options.
|
|
925
1345
|
*/
|
|
926
|
-
getOne(id, options) {
|
|
927
|
-
|
|
928
|
-
if (
|
|
929
|
-
|
|
930
|
-
}
|
|
1346
|
+
async getOne(id, options) {
|
|
1347
|
+
let rawExpand;
|
|
1348
|
+
if (Array.isArray(options?.expand)) {
|
|
1349
|
+
rawExpand = options.expand.join(",");
|
|
1350
|
+
} else if (typeof options?.expand === "string") {
|
|
1351
|
+
rawExpand = options.expand;
|
|
1352
|
+
}
|
|
1353
|
+
const expandInfo = this.normalizeExpand(rawExpand);
|
|
1354
|
+
const fields = this.mergeExpandFields(
|
|
1355
|
+
this.effectiveFields(options?.fields),
|
|
1356
|
+
expandInfo
|
|
1357
|
+
);
|
|
931
1358
|
const qs = new URLSearchParams();
|
|
932
1359
|
if (fields !== void 0) qs.set("fields", fields);
|
|
933
|
-
if (
|
|
1360
|
+
if (rawExpand !== void 0) {
|
|
1361
|
+
qs.set("expand", expandInfo.expand ?? rawExpand);
|
|
1362
|
+
}
|
|
934
1363
|
const qsStr = qs.toString();
|
|
935
|
-
|
|
1364
|
+
const record = await this.http.get(
|
|
936
1365
|
"/" + this.encodeId(this.collectionName) + "/" + this.encodeId(id) + (qsStr ? "?" + qsStr : ""),
|
|
937
1366
|
options
|
|
938
1367
|
);
|
|
1368
|
+
if (record && expandInfo.narrow.size > 0) {
|
|
1369
|
+
return this.applyExpandNarrowing(record, expandInfo.narrow);
|
|
1370
|
+
}
|
|
1371
|
+
return record;
|
|
939
1372
|
}
|
|
940
1373
|
/**
|
|
941
1374
|
* Create a new record.
|
|
@@ -1019,7 +1452,8 @@ var CollectionService = class _CollectionService {
|
|
|
1019
1452
|
this.collectionName,
|
|
1020
1453
|
this.authStore,
|
|
1021
1454
|
this.realtime,
|
|
1022
|
-
schema
|
|
1455
|
+
schema,
|
|
1456
|
+
this.schemaResolver
|
|
1023
1457
|
);
|
|
1024
1458
|
derived.fieldsPreset = this.fieldsPreset;
|
|
1025
1459
|
return derived;
|
|
@@ -2097,6 +2531,10 @@ var LazypockClient = class {
|
|
|
2097
2531
|
this.schemaByName = new Map(
|
|
2098
2532
|
options.types.schemas.map((s) => [s.name, s])
|
|
2099
2533
|
);
|
|
2534
|
+
this.schemaById = /* @__PURE__ */ new Map();
|
|
2535
|
+
for (const s of options.types.schemas) {
|
|
2536
|
+
if (s.id) this.schemaById.set(String(s.id), s);
|
|
2537
|
+
}
|
|
2100
2538
|
}
|
|
2101
2539
|
}
|
|
2102
2540
|
/**
|
|
@@ -2119,7 +2557,10 @@ var LazypockClient = class {
|
|
|
2119
2557
|
this.authStore,
|
|
2120
2558
|
this.realtime,
|
|
2121
2559
|
// Prefer the explicit schema, then the client-level `types.schemas`.
|
|
2122
|
-
schema ?? this.schemaByName?.get(name)
|
|
2560
|
+
schema ?? this.schemaByName?.get(name),
|
|
2561
|
+
// Let the service follow relation fields into other collections
|
|
2562
|
+
// (expand field selection vs nested-relation expansion).
|
|
2563
|
+
(key) => this.schemaByName?.get(key) ?? this.schemaById?.get(key)
|
|
2123
2564
|
);
|
|
2124
2565
|
this.collectionCache.set(name, svc);
|
|
2125
2566
|
}
|
|
@@ -2315,6 +2756,8 @@ function createClient(options) {
|
|
|
2315
2756
|
CollectionService,
|
|
2316
2757
|
CollectionsService,
|
|
2317
2758
|
FilesService,
|
|
2759
|
+
FilterBuilder,
|
|
2760
|
+
FilterExpr,
|
|
2318
2761
|
HttpClient,
|
|
2319
2762
|
LazypockClient,
|
|
2320
2763
|
RealtimeService,
|