lazypock 0.13.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/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,255 @@ 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 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
+ }
816
1030
  /** Warn once when an expand field is not a relation (schema known). */
817
- validateExpand(expand) {
818
- if (!expand || !this.schema?.fields) return;
819
- const relations = new Set(
820
- 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}"`
821
1034
  );
822
- for (const f of expand.split(",")) {
823
- const name = f.trim();
824
- if (name && !relations.has(name)) {
825
- console.warn(
826
- `[lazypock] expand("${name}"): field is not a relation on collection "${this.collectionName}"`
827
- );
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 });
1089
+ }
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
+ }
828
1150
  }
829
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(",");
830
1216
  }
831
1217
  /**
832
1218
  * Fetch a paginated list of records (PocketBase `getList`).
@@ -835,7 +1221,7 @@ var CollectionService = class _CollectionService {
835
1221
  * @param perPage Records per page (default 30).
836
1222
  * @param options Query params (`filter`, `sort`, `expand`, `fields`) + request options.
837
1223
  */
838
- getList(page = 1, perPage = 30, options) {
1224
+ async getList(page = 1, perPage = 30, options) {
839
1225
  const {
840
1226
  requestKey,
841
1227
  autoCancel,
@@ -847,12 +1233,24 @@ var CollectionService = class _CollectionService {
847
1233
  params,
848
1234
  ...queryParams
849
1235
  } = options ?? {};
850
- const fields = this.effectiveFields(queryParams.fields);
851
- if (fields !== void 0 && queryParams.fields === void 0) {
852
- queryParams.fields = fields;
1236
+ if (Array.isArray(queryParams.sort)) {
1237
+ queryParams.sort = queryParams.sort.join(",");
853
1238
  }
854
- if (typeof queryParams.expand === "string") {
855
- this.validateExpand(queryParams.expand);
1239
+ if (Array.isArray(queryParams.expand)) {
1240
+ queryParams.expand = queryParams.expand.join(",");
1241
+ }
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;
856
1254
  }
857
1255
  const qs = new URLSearchParams(
858
1256
  Object.fromEntries(
@@ -863,19 +1261,22 @@ var CollectionService = class _CollectionService {
863
1261
  }).map(([k, v]) => [k, String(v)])
864
1262
  )
865
1263
  ).toString();
866
- return this.http.get(
867
- "/" + this.encodeId(this.collectionName) + "?" + qs,
868
- {
869
- requestKey,
870
- autoCancel,
871
- cancelKey,
872
- fetch,
873
- headers,
874
- signal,
875
- singleFlight,
876
- params
877
- }
878
- );
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;
879
1280
  }
880
1281
  /**
881
1282
  * Fetch all records at once (auto-paginates). Mirrors PocketBase's
@@ -923,19 +1324,32 @@ var CollectionService = class _CollectionService {
923
1324
  * @param id Record ID.
924
1325
  * @param options Optional request options.
925
1326
  */
926
- getOne(id, options) {
927
- const fields = this.effectiveFields(options?.fields);
928
- if (typeof options?.expand === "string") {
929
- this.validateExpand(options.expand);
930
- }
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
+ );
931
1339
  const qs = new URLSearchParams();
932
1340
  if (fields !== void 0) qs.set("fields", fields);
933
- if (typeof options?.expand === "string") qs.set("expand", options.expand);
1341
+ if (rawExpand !== void 0) {
1342
+ qs.set("expand", expandInfo.expand ?? rawExpand);
1343
+ }
934
1344
  const qsStr = qs.toString();
935
- return this.http.get(
1345
+ const record = await this.http.get(
936
1346
  "/" + this.encodeId(this.collectionName) + "/" + this.encodeId(id) + (qsStr ? "?" + qsStr : ""),
937
1347
  options
938
1348
  );
1349
+ if (record && expandInfo.narrow.size > 0) {
1350
+ return this.applyExpandNarrowing(record, expandInfo.narrow);
1351
+ }
1352
+ return record;
939
1353
  }
940
1354
  /**
941
1355
  * Create a new record.
@@ -1019,7 +1433,8 @@ var CollectionService = class _CollectionService {
1019
1433
  this.collectionName,
1020
1434
  this.authStore,
1021
1435
  this.realtime,
1022
- schema
1436
+ schema,
1437
+ this.schemaResolver
1023
1438
  );
1024
1439
  derived.fieldsPreset = this.fieldsPreset;
1025
1440
  return derived;
@@ -2097,6 +2512,10 @@ var LazypockClient = class {
2097
2512
  this.schemaByName = new Map(
2098
2513
  options.types.schemas.map((s) => [s.name, s])
2099
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
+ }
2100
2519
  }
2101
2520
  }
2102
2521
  /**
@@ -2119,7 +2538,10 @@ var LazypockClient = class {
2119
2538
  this.authStore,
2120
2539
  this.realtime,
2121
2540
  // Prefer the explicit schema, then the client-level `types.schemas`.
2122
- 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)
2123
2545
  );
2124
2546
  this.collectionCache.set(name, svc);
2125
2547
  }
@@ -2315,6 +2737,8 @@ function createClient(options) {
2315
2737
  CollectionService,
2316
2738
  CollectionsService,
2317
2739
  FilesService,
2740
+ FilterBuilder,
2741
+ FilterExpr,
2318
2742
  HttpClient,
2319
2743
  LazypockClient,
2320
2744
  RealtimeService,