lazypock 0.10.0 → 0.10.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.d.cts CHANGED
@@ -105,16 +105,106 @@ type FieldKey<T> = Extract<keyof T, string> extends never ? string : Extract<key
105
105
  type FilterOp = "=" | "!=" | "~" | "!~" | ">" | ">=" | "<" | "<=";
106
106
  /** Optional whitespace around a filter operator (`project=x` and `project = x` both compile). */
107
107
  type FilterWs = "" | " ";
108
+ /** Trim one-or-more leading/trailing spaces off a string literal. */
109
+ type Trim<S extends string> = S extends ` ${infer R}` ? Trim<R> : S extends `${infer L} ` ? Trim<L> : S;
110
+ /** `never` when the string contains a single-quote. */
111
+ type NoQuote<S extends string> = S extends `${string}'${string}` ? never : S;
112
+ /** `never` when the string contains a double-quote. */
113
+ type NoDQuote<S extends string> = S extends `${string}"${string}` ? never : S;
114
+ /** Single decimal digits — used to consume numeric values char-by-char. */
115
+ type Digits = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9";
108
116
  /**
109
- * A filterable field: a top-level key, or a dot-path into a relation
110
- * (`author.email = 'x'`).
117
+ * Consume a leading number (digits plus an optional decimal part) and
118
+ * return the remainder. `\`${number}\`` is NOT used in template positions:
119
+ * it matches a single digit, leaving `"0"` behind for `"10"`.
111
120
  */
112
- type FilterField<T> = `${FieldKey<T>}${"" | `.${string}`}`;
121
+ type ConsumeNum<S extends string> = S extends `${infer C}${infer R}` ? C extends Digits ? ConsumeNum<R> : C extends "." ? R extends `${Digits}${string}` ? ConsumeNum<R> : S : S : S;
113
122
  /**
114
- * One `field op value` clause. Spaces around the operator are optional
115
- * (`project=x` / `project = x`) — the backend treats them the same.
123
+ * Consume chars up to (not including) the first banned char and return the
124
+ * remainder. Bounds dot-paths so a path can't swallow a following clause.
116
125
  */
117
- type FilterClause<T> = `${FilterField<T>}${FilterWs}${FilterOp}${FilterWs}${string}`;
126
+ type ConsumeUntil<S extends string, Ban extends string> = S extends `${infer C}${infer R}` ? C extends Ban ? S : ConsumeUntil<R, Ban> : S;
127
+ /** Chars that terminate a relation dot-path. */
128
+ type PathBan = " " | "&" | "|" | "(" | ")" | "'" | "\"";
129
+ /**
130
+ * A single expandable token: a top-level key, or a dot-path into a relation
131
+ * (`author.user.profile`). Only the first segment is validated — nested
132
+ * segments reference fields of the *target* collection, which `T` doesn't
133
+ * describe.
134
+ */
135
+ type ExpandField<T> = `${FieldKey<T>}${"" | `.${string}`}`;
136
+ /**
137
+ * Validate every comma-separated expand token; returns the original literal
138
+ * `O` when all tokens are valid, `never` otherwise. Spaces around commas are
139
+ * tolerated (the runtime trims tokens too).
140
+ */
141
+ type ValidExpand<T, E extends string, O extends string = E> = string extends FieldKey<T> ? O : E extends `${infer Head},${infer Tail}` ? Trim<Head> extends ExpandField<T> ? ValidExpand<T, Trim<Tail>, O> : never : Trim<E> extends ExpandField<T> ? O : never;
142
+ /**
143
+ * Type-checked expand string: comma-separated relation field names,
144
+ * including nested dot-paths (e.g. `"author"`, `"author,category"`,
145
+ * `"author.user.profile"`). **Every** comma-separated token is validated —
146
+ * `expand: "author, nope"` is a compile error, not just the first token.
147
+ * Non-relation fields are warned about at runtime when a schema is
148
+ * available.
149
+ */
150
+ type ExpandString<T, E extends string = never> = E & (E extends ValidExpand<T, E, E> ? E : never);
151
+ /** A single `[+|-]field` sort token. */
152
+ type SortField<T> = `${"" | "-" | "+"}${FieldKey<T>}`;
153
+ /**
154
+ * Validate every comma-separated sort token; returns the original literal
155
+ * `O` when all tokens are valid, `never` otherwise.
156
+ */
157
+ type ValidSort<T, E extends string, O extends string = E> = string extends FieldKey<T> ? O : E extends `${infer Head},${infer Tail}` ? Trim<Head> extends SortField<T> ? ValidSort<T, Trim<Tail>, O> : never : Trim<E> extends SortField<T> ? O : never;
158
+ /**
159
+ * Type-checked sort string: `field`, `-field` (desc), `+field`, or
160
+ * comma-separated combinations (e.g. `"-created,title"`). **Every** token is
161
+ * validated — `sort: "title, nope"` is a compile error.
162
+ */
163
+ type SortString<T, E extends string = never> = E & (E extends ValidSort<T, E, E> ? E : never);
164
+ /**
165
+ * A string value with no quotes, `&&`/`||`, or parens — the "bare" fallback
166
+ * for filter values (numbers, `true`/`false`/`null`, unquoted text).
167
+ * Applied against the *same* literal (`S extends SimpleVal<S>`) so the
168
+ * banned-char checks run on the concrete substring, not on the abstract
169
+ * `string` type.
170
+ */
171
+ type SimpleVal<S extends string> = (S extends `${string}'${string}` ? never : S) & (S extends `${string}"${string}` ? never : S) & (S extends `${string}&&${string}` ? never : S) & (S extends `${string}||${string}` ? never : S) & (S extends `${string}(${string}` ? never : S) & (S extends `${string})${string}` ? never : S);
172
+ /**
173
+ * Field-reference value (`title = author` / `title = author.email`): a
174
+ * top-level key, optionally followed by a bounded dot-path. Returns the
175
+ * remainder after the value.
176
+ */
177
+ type FFieldRef<T, S extends string> = S extends `${FieldKey<T>}${infer R}` ? R extends `.${infer Rest}` ? ConsumeUntil<Rest, PathBan> : R : never;
178
+ /**
179
+ * Consume one filter value (quoted string, number, boolean/null, field
180
+ * reference, or bare text) and return the remainder — the input left for the
181
+ * `&&`/`||`/`)`/end checks upstream.
182
+ */
183
+ type FValue<T, S extends string> = (S extends `'${infer V}'${infer R}` ? (V extends NoQuote<V> ? R : never) : never) | (S extends `"${infer V}"${infer R}` ? (V extends NoDQuote<V> ? R : never) : never) | (S extends `${"true" | "false" | "null"}${infer R}` ? R : never) | (S extends `-${infer Rest}` ? Rest extends `${Digits}${string}` ? ConsumeNum<Rest> : never : S extends `${Digits}${string}` ? ConsumeNum<S> : never) | FFieldRef<T, S> | (S extends SimpleVal<S> ? "" : never);
184
+ /**
185
+ * Parse one `field op value` clause — either a plain top-level field or a
186
+ * bounded relation dot-path — and return the remainder. Spaces around the
187
+ * operator are optional (`project=x` / `project = x`).
188
+ */
189
+ type FClause<T, S extends string> = S extends `${FieldKey<T>}${infer R}` ? R extends `${FilterWs}${FilterOp}${FilterWs}${infer R2}` ? FValue<T, Trim<R2>> : R extends `.${infer Rest}` ? ConsumeUntil<Rest, PathBan> extends infer R2 ? R2 extends `${FilterWs}${FilterOp}${FilterWs}${infer R3}` ? FValue<T, Trim<R3>> : never : never : never : never;
190
+ /** A clause or a parenthesised group; parens must balance and close. */
191
+ type FAtom<T, S extends string> = FClause<T, S> | (S extends `(${infer Inner}` ? FOr<T, Inner> extends infer R ? R extends `)${infer Tail}` ? Tail : never : never : never);
192
+ /** Optional `!` negation in front of an atom. */
193
+ type FNot<T, S extends string> = S extends `!${infer Tail}` ? FAtom<T, Tail> : FAtom<T, S>;
194
+ /**
195
+ * One or more atoms joined by `&&`. Returns `never` on parse failure (a
196
+ * failed atom must not masquerade as an empty remainder — `never extends ""`
197
+ * is true), the empty string on success, or the unconsumed remainder.
198
+ */
199
+ type FAnd<T, S extends string> = FNot<T, S> extends infer R ? [R] extends [never] ? never : R extends "" ? "" : R extends `${FilterWs}&&${FilterWs}${infer Tail}` ? FAnd<T, Tail> : R : never;
200
+ /** One or more `&&`-chains joined by `||` — the top-level expression. */
201
+ type FOr<T, S extends string> = FAnd<T, S> extends infer R ? [R] extends [never] ? never : R extends "" ? "" : R extends `${FilterWs}||${FilterWs}${infer Tail}` ? FOr<T, Tail> : R : never;
202
+ /**
203
+ * Validate a full filter expression: the parser must consume the entire
204
+ * (trimmed) string. Compares the *literal* against the parse result rather
205
+ * than `extends ""` so a `never` parse failure can't satisfy the check.
206
+ */
207
+ type ValidFilter<T, F extends string, O extends string = F> = string extends FieldKey<T> ? O : "" extends FOr<T, Trim<F>> ? O : never;
118
208
  /**
119
209
  * Type-checked filter expression (PocketBase syntax).
120
210
  *
@@ -125,52 +215,62 @@ type FilterClause<T> = `${FilterField<T>}${FilterWs}${FilterOp}${FilterWs}${stri
125
215
  * getList(1, 20, { filter: "author.email = 'x'" }) // relation dot-path
126
216
  * ```
127
217
  *
128
- * The expression must start with a valid field name — optionally preceded by
129
- * `!` / `(` / `!(` — followed by a valid operator; the rest (values,
130
- * `&&`/`||`, `!`, parens) is free-form. Field names and operators are
131
- * suggested by the editor as you type.
132
- */
133
- type FilterString<T> = `${FilterClause<T>}${string}` | `${"!" | "(" | "!("}${FilterClause<T>}${string}`;
134
- /** A single `[+|-]field` sort token. */
135
- type SortField<T> = `${"" | "-" | "+"}${FieldKey<T>}`;
136
- /**
137
- * Type-checked sort string: `field`, `-field` (desc), `+field`, or
138
- * comma-separated combinations (e.g. `"-created,title"`).
218
+ * **Every** `field op value` clause is validated — the field name, the
219
+ * operator, and the clause structure (`&&`, `||`, `!`, parens, and nested
220
+ * groups all typecheck). Quoted values may contain `&&`/`||` (e.g.
221
+ * `title ~ 'a && b'`); bare unquoted values are bounded so they can't hide a
222
+ * following clause. When the expression is a plain dynamic `string` (e.g.
223
+ * `filter: \`title=${search}\``), the guard falls back to accepting it
224
+ * verbatim — the server decides at runtime.
139
225
  */
140
- type SortString<T> = SortField<T> | `${SortField<T>},${string}`;
226
+ type FilterString<T, F extends string = never> = F & (F extends ValidFilter<T, F, F> ? F : string extends F ? F : never);
141
227
  /**
142
- * Type-checked expand string: comma-separated relation field names,
143
- * including nested dot-paths (e.g. `"author"`, `"author,category"`,
144
- * `"author.user.profile"`). Non-relation fields are warned about at
145
- * runtime when a schema is available.
228
+ * First segment of a single expand token (`"author.user"` → `"author"`) —
229
+ * the key PocketBase uses on the expanded record.
146
230
  */
147
- type ExpandField<T> = `${FieldKey<T>}${"" | `.${string}`}`;
231
+ type TopExpandKey<S extends string> = S extends `${infer A}.${string}` ? A : S;
232
+ /** Top-level keys of a comma-separated expand literal (`"a,b"` → `"a" | "b"`). */
233
+ type ExpandKeys<S extends string> = S extends "" ? never : S extends `${infer H},${infer T}` ? TopExpandKey<H> | ExpandKeys<T> : TopExpandKey<S>;
148
234
  /**
149
- * Type-checked expand string: comma-separated relation field names,
150
- * including nested dot-paths (e.g. `"author"`, `"author,category"`,
151
- * `"author.user.profile"`).
235
+ * The `expand` property attached to records when `expand` is used. Keys are
236
+ * parsed from the caller's expand literal (dot-paths collapse to their first
237
+ * segment), so `record.expand.user` typechecks and the nested record is
238
+ * `unknown` (its shape belongs to the target collection). Falls back to
239
+ * `Record<string, unknown>` when the expand value is dynamic or absent.
152
240
  */
153
- type ExpandString<T> = ExpandField<T> | `${ExpandField<T>},${string}`;
241
+ type ExpandObj<E extends string> = [
242
+ E
243
+ ] extends [never] ? Record<string, unknown> : [
244
+ string
245
+ ] extends [E] ? Record<string, unknown> : Partial<Record<ExpandKeys<E>, unknown>>;
154
246
  /**
155
247
  * Query options for list/read operations, typed against a record shape `T`.
156
248
  *
157
249
  * `filter`, `sort`, `expand` and `fields` are recognized; any other key is
158
250
  * passed through as a raw query parameter (PocketBase-compatible).
251
+ *
252
+ * The `E`/`S`/`F` generics are inferred from the caller's literals; the
253
+ * {@link CollectionService} methods constrain them with the validated
254
+ * {@link ExpandString} / {@link SortString} / {@link FilterString} guards.
255
+ */
256
+ /**
257
+ * `_T` is kept for backward compatibility (`ListOptions<Post>`) but the
258
+ * per-field validation now runs through the `E`/`S`/`F` generics.
159
259
  */
160
- interface ListOptions<T = ApiRecord> {
260
+ interface ListOptions<_T = ApiRecord, E extends string = never, S extends string = never, F extends string = never> {
161
261
  /**
162
262
  * PocketBase filter expression. Field names + operators are type-checked
163
263
  * when `T` is a concrete shape.
164
264
  */
165
- filter?: FilterString<T>;
265
+ filter?: FilterString<_T, F>;
166
266
  /**
167
267
  * Sort field(s): `field`, `-field` (descending), comma-separated.
168
268
  */
169
- sort?: SortString<T>;
269
+ sort?: SortString<_T, S>;
170
270
  /**
171
271
  * Comma-separated relation field names to expand.
172
272
  */
173
- expand?: ExpandString<T>;
273
+ expand?: ExpandString<_T, E>;
174
274
  /**
175
275
  * Explicit field projection (overrides {@link CollectionService.select}).
176
276
  */
@@ -183,9 +283,9 @@ interface ListOptions<T = ApiRecord> {
183
283
  /**
184
284
  * Options for single-record reads (`getOne`): expand + explicit fields.
185
285
  */
186
- interface ReadOptions<T = ApiRecord> {
286
+ interface ReadOptions<_T = ApiRecord, E extends string = never> {
187
287
  /** Comma-separated relation field names to expand. */
188
- expand?: ExpandString<T>;
288
+ expand?: ExpandString<_T, E>;
189
289
  /** Explicit field projection (overrides {@link CollectionService.select}). */
190
290
  fields?: string;
191
291
  }
@@ -534,8 +634,14 @@ interface RealtimeSubscribeOptions {
534
634
  * Get an instance via {@link LazypockClient.collection}.
535
635
  *
536
636
  * @typeParam T — The record shape for this collection. Defaults to {@link ApiRecord}.
637
+ * @typeParam TData — Write-only create/update payload shape (defaults to `never`,
638
+ * which falls back to `T`-derived create data).
639
+ * @typeParam TFields — The key set accepted by `filter`/`sort`/`expand`/`select`.
640
+ * Defaults to `T`. The codegen client binds a *QueryFields type that includes
641
+ * hidden fields — excluded from the read model but still expandable/filterable
642
+ * at runtime — so `expand: "hiddenRelation"` typechecks.
537
643
  */
538
- declare class CollectionService<T = ApiRecord, TData = never> {
644
+ declare class CollectionService<T = ApiRecord, TData = never, TFields = T> {
539
645
  private http;
540
646
  private collectionName;
541
647
  private authStore?;
@@ -574,7 +680,7 @@ declare class CollectionService<T = ApiRecord, TData = never> {
574
680
  * `fields` param — the server returns every non-password field.
575
681
  * Pass `select("*")` to restore the default after a projection.
576
682
  */
577
- select<K extends FieldKey<T> | "*">(...fields: K[]): CollectionService<T, TData>;
683
+ select<K extends FieldKey<TFields> | "*">(...fields: K[]): CollectionService<T, TData, TFields>;
578
684
  /**
579
685
  * Fields to send with reads:
580
686
  * 1. explicit `options.fields` (caller wins)
@@ -607,27 +713,35 @@ declare class CollectionService<T = ApiRecord, TData = never> {
607
713
  * @param perPage Records per page (default 30).
608
714
  * @param options Query params (`filter`, `sort`, `expand`, `fields`) + request options.
609
715
  */
610
- getList<T2 = T>(page?: number, perPage?: number, options?: ListOptions<T> & RequestOptions): Promise<ListResult<T2> | null>;
716
+ getList<T2 = T, E extends string = never, S extends string = never, F extends string = never>(page?: number, perPage?: number, options?: ListOptions<TFields, E, S, F> & RequestOptions): Promise<ListResult<T2 & {
717
+ expand?: ExpandObj<E>;
718
+ }> | null>;
611
719
  /**
612
720
  * Fetch all records at once (auto-paginates). Mirrors PocketBase's
613
721
  * `pb.collection(name).getFullList()`.
614
722
  *
615
723
  * @param options Query params (`sort`, `filter`, `batch`, etc.) + request options.
616
724
  */
617
- getFullList<T2 = T>(options?: ListOptions<T> & RequestOptions): Promise<Array<T2>>;
725
+ getFullList<T2 = T, E extends string = never, S extends string = never, F extends string = never>(options?: ListOptions<TFields, E, S, F> & RequestOptions): Promise<Array<T2 & {
726
+ expand?: ExpandObj<E>;
727
+ }>>;
618
728
  /**
619
729
  * Fetch the first record matching a filter (PocketBase `getFirstListItem`).
620
730
  *
621
731
  * @param filter Filter expression (e.g. `title = 'x'`).
622
732
  * @param options Optional request options.
623
733
  */
624
- getFirstListItem<T2 = T>(filter: FilterString<T>, options?: ListOptions<T> & RequestOptions): Promise<T2 | null>;
734
+ getFirstListItem<T2 = T, F extends string = never, E extends string = never, S extends string = never>(filter: FilterString<TFields, F>, options?: ListOptions<TFields, E, S, F> & RequestOptions): Promise<(T2 & {
735
+ expand?: ExpandObj<E>;
736
+ }) | null>;
625
737
  /**
626
738
  * Get a single record by ID.
627
739
  * @param id Record ID.
628
740
  * @param options Optional request options.
629
741
  */
630
- getOne(id: string, options?: ReadOptions<T> & RequestOptions): Promise<T | null>;
742
+ getOne<E extends string = never>(id: string, options?: ReadOptions<TFields, E> & RequestOptions): Promise<(T & {
743
+ expand?: ExpandObj<E>;
744
+ }) | null>;
631
745
  /**
632
746
  * Create a new record.
633
747
  * @param data Record fields. When `T` is a concrete shape (e.g. a generated
@@ -680,7 +794,7 @@ declare class CollectionService<T = ApiRecord, TData = never> {
680
794
  * `types.schemas`). Enables schema-aware defaults: hidden fields are
681
795
  * excluded from responses, select/expand are validated.
682
796
  */
683
- withSchema(schema: CollectionSchema): CollectionService<T, TData>;
797
+ withSchema(schema: CollectionSchema): CollectionService<T, TData, TFields>;
684
798
  /**
685
799
  * Subscribe to realtime changes for this collection.
686
800
  * The event's `action` is one of `"create" | "update" | "delete"` and the
package/dist/index.d.ts CHANGED
@@ -105,16 +105,106 @@ type FieldKey<T> = Extract<keyof T, string> extends never ? string : Extract<key
105
105
  type FilterOp = "=" | "!=" | "~" | "!~" | ">" | ">=" | "<" | "<=";
106
106
  /** Optional whitespace around a filter operator (`project=x` and `project = x` both compile). */
107
107
  type FilterWs = "" | " ";
108
+ /** Trim one-or-more leading/trailing spaces off a string literal. */
109
+ type Trim<S extends string> = S extends ` ${infer R}` ? Trim<R> : S extends `${infer L} ` ? Trim<L> : S;
110
+ /** `never` when the string contains a single-quote. */
111
+ type NoQuote<S extends string> = S extends `${string}'${string}` ? never : S;
112
+ /** `never` when the string contains a double-quote. */
113
+ type NoDQuote<S extends string> = S extends `${string}"${string}` ? never : S;
114
+ /** Single decimal digits — used to consume numeric values char-by-char. */
115
+ type Digits = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9";
108
116
  /**
109
- * A filterable field: a top-level key, or a dot-path into a relation
110
- * (`author.email = 'x'`).
117
+ * Consume a leading number (digits plus an optional decimal part) and
118
+ * return the remainder. `\`${number}\`` is NOT used in template positions:
119
+ * it matches a single digit, leaving `"0"` behind for `"10"`.
111
120
  */
112
- type FilterField<T> = `${FieldKey<T>}${"" | `.${string}`}`;
121
+ type ConsumeNum<S extends string> = S extends `${infer C}${infer R}` ? C extends Digits ? ConsumeNum<R> : C extends "." ? R extends `${Digits}${string}` ? ConsumeNum<R> : S : S : S;
113
122
  /**
114
- * One `field op value` clause. Spaces around the operator are optional
115
- * (`project=x` / `project = x`) — the backend treats them the same.
123
+ * Consume chars up to (not including) the first banned char and return the
124
+ * remainder. Bounds dot-paths so a path can't swallow a following clause.
116
125
  */
117
- type FilterClause<T> = `${FilterField<T>}${FilterWs}${FilterOp}${FilterWs}${string}`;
126
+ type ConsumeUntil<S extends string, Ban extends string> = S extends `${infer C}${infer R}` ? C extends Ban ? S : ConsumeUntil<R, Ban> : S;
127
+ /** Chars that terminate a relation dot-path. */
128
+ type PathBan = " " | "&" | "|" | "(" | ")" | "'" | "\"";
129
+ /**
130
+ * A single expandable token: a top-level key, or a dot-path into a relation
131
+ * (`author.user.profile`). Only the first segment is validated — nested
132
+ * segments reference fields of the *target* collection, which `T` doesn't
133
+ * describe.
134
+ */
135
+ type ExpandField<T> = `${FieldKey<T>}${"" | `.${string}`}`;
136
+ /**
137
+ * Validate every comma-separated expand token; returns the original literal
138
+ * `O` when all tokens are valid, `never` otherwise. Spaces around commas are
139
+ * tolerated (the runtime trims tokens too).
140
+ */
141
+ type ValidExpand<T, E extends string, O extends string = E> = string extends FieldKey<T> ? O : E extends `${infer Head},${infer Tail}` ? Trim<Head> extends ExpandField<T> ? ValidExpand<T, Trim<Tail>, O> : never : Trim<E> extends ExpandField<T> ? O : never;
142
+ /**
143
+ * Type-checked expand string: comma-separated relation field names,
144
+ * including nested dot-paths (e.g. `"author"`, `"author,category"`,
145
+ * `"author.user.profile"`). **Every** comma-separated token is validated —
146
+ * `expand: "author, nope"` is a compile error, not just the first token.
147
+ * Non-relation fields are warned about at runtime when a schema is
148
+ * available.
149
+ */
150
+ type ExpandString<T, E extends string = never> = E & (E extends ValidExpand<T, E, E> ? E : never);
151
+ /** A single `[+|-]field` sort token. */
152
+ type SortField<T> = `${"" | "-" | "+"}${FieldKey<T>}`;
153
+ /**
154
+ * Validate every comma-separated sort token; returns the original literal
155
+ * `O` when all tokens are valid, `never` otherwise.
156
+ */
157
+ type ValidSort<T, E extends string, O extends string = E> = string extends FieldKey<T> ? O : E extends `${infer Head},${infer Tail}` ? Trim<Head> extends SortField<T> ? ValidSort<T, Trim<Tail>, O> : never : Trim<E> extends SortField<T> ? O : never;
158
+ /**
159
+ * Type-checked sort string: `field`, `-field` (desc), `+field`, or
160
+ * comma-separated combinations (e.g. `"-created,title"`). **Every** token is
161
+ * validated — `sort: "title, nope"` is a compile error.
162
+ */
163
+ type SortString<T, E extends string = never> = E & (E extends ValidSort<T, E, E> ? E : never);
164
+ /**
165
+ * A string value with no quotes, `&&`/`||`, or parens — the "bare" fallback
166
+ * for filter values (numbers, `true`/`false`/`null`, unquoted text).
167
+ * Applied against the *same* literal (`S extends SimpleVal<S>`) so the
168
+ * banned-char checks run on the concrete substring, not on the abstract
169
+ * `string` type.
170
+ */
171
+ type SimpleVal<S extends string> = (S extends `${string}'${string}` ? never : S) & (S extends `${string}"${string}` ? never : S) & (S extends `${string}&&${string}` ? never : S) & (S extends `${string}||${string}` ? never : S) & (S extends `${string}(${string}` ? never : S) & (S extends `${string})${string}` ? never : S);
172
+ /**
173
+ * Field-reference value (`title = author` / `title = author.email`): a
174
+ * top-level key, optionally followed by a bounded dot-path. Returns the
175
+ * remainder after the value.
176
+ */
177
+ type FFieldRef<T, S extends string> = S extends `${FieldKey<T>}${infer R}` ? R extends `.${infer Rest}` ? ConsumeUntil<Rest, PathBan> : R : never;
178
+ /**
179
+ * Consume one filter value (quoted string, number, boolean/null, field
180
+ * reference, or bare text) and return the remainder — the input left for the
181
+ * `&&`/`||`/`)`/end checks upstream.
182
+ */
183
+ type FValue<T, S extends string> = (S extends `'${infer V}'${infer R}` ? (V extends NoQuote<V> ? R : never) : never) | (S extends `"${infer V}"${infer R}` ? (V extends NoDQuote<V> ? R : never) : never) | (S extends `${"true" | "false" | "null"}${infer R}` ? R : never) | (S extends `-${infer Rest}` ? Rest extends `${Digits}${string}` ? ConsumeNum<Rest> : never : S extends `${Digits}${string}` ? ConsumeNum<S> : never) | FFieldRef<T, S> | (S extends SimpleVal<S> ? "" : never);
184
+ /**
185
+ * Parse one `field op value` clause — either a plain top-level field or a
186
+ * bounded relation dot-path — and return the remainder. Spaces around the
187
+ * operator are optional (`project=x` / `project = x`).
188
+ */
189
+ type FClause<T, S extends string> = S extends `${FieldKey<T>}${infer R}` ? R extends `${FilterWs}${FilterOp}${FilterWs}${infer R2}` ? FValue<T, Trim<R2>> : R extends `.${infer Rest}` ? ConsumeUntil<Rest, PathBan> extends infer R2 ? R2 extends `${FilterWs}${FilterOp}${FilterWs}${infer R3}` ? FValue<T, Trim<R3>> : never : never : never : never;
190
+ /** A clause or a parenthesised group; parens must balance and close. */
191
+ type FAtom<T, S extends string> = FClause<T, S> | (S extends `(${infer Inner}` ? FOr<T, Inner> extends infer R ? R extends `)${infer Tail}` ? Tail : never : never : never);
192
+ /** Optional `!` negation in front of an atom. */
193
+ type FNot<T, S extends string> = S extends `!${infer Tail}` ? FAtom<T, Tail> : FAtom<T, S>;
194
+ /**
195
+ * One or more atoms joined by `&&`. Returns `never` on parse failure (a
196
+ * failed atom must not masquerade as an empty remainder — `never extends ""`
197
+ * is true), the empty string on success, or the unconsumed remainder.
198
+ */
199
+ type FAnd<T, S extends string> = FNot<T, S> extends infer R ? [R] extends [never] ? never : R extends "" ? "" : R extends `${FilterWs}&&${FilterWs}${infer Tail}` ? FAnd<T, Tail> : R : never;
200
+ /** One or more `&&`-chains joined by `||` — the top-level expression. */
201
+ type FOr<T, S extends string> = FAnd<T, S> extends infer R ? [R] extends [never] ? never : R extends "" ? "" : R extends `${FilterWs}||${FilterWs}${infer Tail}` ? FOr<T, Tail> : R : never;
202
+ /**
203
+ * Validate a full filter expression: the parser must consume the entire
204
+ * (trimmed) string. Compares the *literal* against the parse result rather
205
+ * than `extends ""` so a `never` parse failure can't satisfy the check.
206
+ */
207
+ type ValidFilter<T, F extends string, O extends string = F> = string extends FieldKey<T> ? O : "" extends FOr<T, Trim<F>> ? O : never;
118
208
  /**
119
209
  * Type-checked filter expression (PocketBase syntax).
120
210
  *
@@ -125,52 +215,62 @@ type FilterClause<T> = `${FilterField<T>}${FilterWs}${FilterOp}${FilterWs}${stri
125
215
  * getList(1, 20, { filter: "author.email = 'x'" }) // relation dot-path
126
216
  * ```
127
217
  *
128
- * The expression must start with a valid field name — optionally preceded by
129
- * `!` / `(` / `!(` — followed by a valid operator; the rest (values,
130
- * `&&`/`||`, `!`, parens) is free-form. Field names and operators are
131
- * suggested by the editor as you type.
132
- */
133
- type FilterString<T> = `${FilterClause<T>}${string}` | `${"!" | "(" | "!("}${FilterClause<T>}${string}`;
134
- /** A single `[+|-]field` sort token. */
135
- type SortField<T> = `${"" | "-" | "+"}${FieldKey<T>}`;
136
- /**
137
- * Type-checked sort string: `field`, `-field` (desc), `+field`, or
138
- * comma-separated combinations (e.g. `"-created,title"`).
218
+ * **Every** `field op value` clause is validated — the field name, the
219
+ * operator, and the clause structure (`&&`, `||`, `!`, parens, and nested
220
+ * groups all typecheck). Quoted values may contain `&&`/`||` (e.g.
221
+ * `title ~ 'a && b'`); bare unquoted values are bounded so they can't hide a
222
+ * following clause. When the expression is a plain dynamic `string` (e.g.
223
+ * `filter: \`title=${search}\``), the guard falls back to accepting it
224
+ * verbatim — the server decides at runtime.
139
225
  */
140
- type SortString<T> = SortField<T> | `${SortField<T>},${string}`;
226
+ type FilterString<T, F extends string = never> = F & (F extends ValidFilter<T, F, F> ? F : string extends F ? F : never);
141
227
  /**
142
- * Type-checked expand string: comma-separated relation field names,
143
- * including nested dot-paths (e.g. `"author"`, `"author,category"`,
144
- * `"author.user.profile"`). Non-relation fields are warned about at
145
- * runtime when a schema is available.
228
+ * First segment of a single expand token (`"author.user"` → `"author"`) —
229
+ * the key PocketBase uses on the expanded record.
146
230
  */
147
- type ExpandField<T> = `${FieldKey<T>}${"" | `.${string}`}`;
231
+ type TopExpandKey<S extends string> = S extends `${infer A}.${string}` ? A : S;
232
+ /** Top-level keys of a comma-separated expand literal (`"a,b"` → `"a" | "b"`). */
233
+ type ExpandKeys<S extends string> = S extends "" ? never : S extends `${infer H},${infer T}` ? TopExpandKey<H> | ExpandKeys<T> : TopExpandKey<S>;
148
234
  /**
149
- * Type-checked expand string: comma-separated relation field names,
150
- * including nested dot-paths (e.g. `"author"`, `"author,category"`,
151
- * `"author.user.profile"`).
235
+ * The `expand` property attached to records when `expand` is used. Keys are
236
+ * parsed from the caller's expand literal (dot-paths collapse to their first
237
+ * segment), so `record.expand.user` typechecks and the nested record is
238
+ * `unknown` (its shape belongs to the target collection). Falls back to
239
+ * `Record<string, unknown>` when the expand value is dynamic or absent.
152
240
  */
153
- type ExpandString<T> = ExpandField<T> | `${ExpandField<T>},${string}`;
241
+ type ExpandObj<E extends string> = [
242
+ E
243
+ ] extends [never] ? Record<string, unknown> : [
244
+ string
245
+ ] extends [E] ? Record<string, unknown> : Partial<Record<ExpandKeys<E>, unknown>>;
154
246
  /**
155
247
  * Query options for list/read operations, typed against a record shape `T`.
156
248
  *
157
249
  * `filter`, `sort`, `expand` and `fields` are recognized; any other key is
158
250
  * passed through as a raw query parameter (PocketBase-compatible).
251
+ *
252
+ * The `E`/`S`/`F` generics are inferred from the caller's literals; the
253
+ * {@link CollectionService} methods constrain them with the validated
254
+ * {@link ExpandString} / {@link SortString} / {@link FilterString} guards.
255
+ */
256
+ /**
257
+ * `_T` is kept for backward compatibility (`ListOptions<Post>`) but the
258
+ * per-field validation now runs through the `E`/`S`/`F` generics.
159
259
  */
160
- interface ListOptions<T = ApiRecord> {
260
+ interface ListOptions<_T = ApiRecord, E extends string = never, S extends string = never, F extends string = never> {
161
261
  /**
162
262
  * PocketBase filter expression. Field names + operators are type-checked
163
263
  * when `T` is a concrete shape.
164
264
  */
165
- filter?: FilterString<T>;
265
+ filter?: FilterString<_T, F>;
166
266
  /**
167
267
  * Sort field(s): `field`, `-field` (descending), comma-separated.
168
268
  */
169
- sort?: SortString<T>;
269
+ sort?: SortString<_T, S>;
170
270
  /**
171
271
  * Comma-separated relation field names to expand.
172
272
  */
173
- expand?: ExpandString<T>;
273
+ expand?: ExpandString<_T, E>;
174
274
  /**
175
275
  * Explicit field projection (overrides {@link CollectionService.select}).
176
276
  */
@@ -183,9 +283,9 @@ interface ListOptions<T = ApiRecord> {
183
283
  /**
184
284
  * Options for single-record reads (`getOne`): expand + explicit fields.
185
285
  */
186
- interface ReadOptions<T = ApiRecord> {
286
+ interface ReadOptions<_T = ApiRecord, E extends string = never> {
187
287
  /** Comma-separated relation field names to expand. */
188
- expand?: ExpandString<T>;
288
+ expand?: ExpandString<_T, E>;
189
289
  /** Explicit field projection (overrides {@link CollectionService.select}). */
190
290
  fields?: string;
191
291
  }
@@ -534,8 +634,14 @@ interface RealtimeSubscribeOptions {
534
634
  * Get an instance via {@link LazypockClient.collection}.
535
635
  *
536
636
  * @typeParam T — The record shape for this collection. Defaults to {@link ApiRecord}.
637
+ * @typeParam TData — Write-only create/update payload shape (defaults to `never`,
638
+ * which falls back to `T`-derived create data).
639
+ * @typeParam TFields — The key set accepted by `filter`/`sort`/`expand`/`select`.
640
+ * Defaults to `T`. The codegen client binds a *QueryFields type that includes
641
+ * hidden fields — excluded from the read model but still expandable/filterable
642
+ * at runtime — so `expand: "hiddenRelation"` typechecks.
537
643
  */
538
- declare class CollectionService<T = ApiRecord, TData = never> {
644
+ declare class CollectionService<T = ApiRecord, TData = never, TFields = T> {
539
645
  private http;
540
646
  private collectionName;
541
647
  private authStore?;
@@ -574,7 +680,7 @@ declare class CollectionService<T = ApiRecord, TData = never> {
574
680
  * `fields` param — the server returns every non-password field.
575
681
  * Pass `select("*")` to restore the default after a projection.
576
682
  */
577
- select<K extends FieldKey<T> | "*">(...fields: K[]): CollectionService<T, TData>;
683
+ select<K extends FieldKey<TFields> | "*">(...fields: K[]): CollectionService<T, TData, TFields>;
578
684
  /**
579
685
  * Fields to send with reads:
580
686
  * 1. explicit `options.fields` (caller wins)
@@ -607,27 +713,35 @@ declare class CollectionService<T = ApiRecord, TData = never> {
607
713
  * @param perPage Records per page (default 30).
608
714
  * @param options Query params (`filter`, `sort`, `expand`, `fields`) + request options.
609
715
  */
610
- getList<T2 = T>(page?: number, perPage?: number, options?: ListOptions<T> & RequestOptions): Promise<ListResult<T2> | null>;
716
+ getList<T2 = T, E extends string = never, S extends string = never, F extends string = never>(page?: number, perPage?: number, options?: ListOptions<TFields, E, S, F> & RequestOptions): Promise<ListResult<T2 & {
717
+ expand?: ExpandObj<E>;
718
+ }> | null>;
611
719
  /**
612
720
  * Fetch all records at once (auto-paginates). Mirrors PocketBase's
613
721
  * `pb.collection(name).getFullList()`.
614
722
  *
615
723
  * @param options Query params (`sort`, `filter`, `batch`, etc.) + request options.
616
724
  */
617
- getFullList<T2 = T>(options?: ListOptions<T> & RequestOptions): Promise<Array<T2>>;
725
+ getFullList<T2 = T, E extends string = never, S extends string = never, F extends string = never>(options?: ListOptions<TFields, E, S, F> & RequestOptions): Promise<Array<T2 & {
726
+ expand?: ExpandObj<E>;
727
+ }>>;
618
728
  /**
619
729
  * Fetch the first record matching a filter (PocketBase `getFirstListItem`).
620
730
  *
621
731
  * @param filter Filter expression (e.g. `title = 'x'`).
622
732
  * @param options Optional request options.
623
733
  */
624
- getFirstListItem<T2 = T>(filter: FilterString<T>, options?: ListOptions<T> & RequestOptions): Promise<T2 | null>;
734
+ getFirstListItem<T2 = T, F extends string = never, E extends string = never, S extends string = never>(filter: FilterString<TFields, F>, options?: ListOptions<TFields, E, S, F> & RequestOptions): Promise<(T2 & {
735
+ expand?: ExpandObj<E>;
736
+ }) | null>;
625
737
  /**
626
738
  * Get a single record by ID.
627
739
  * @param id Record ID.
628
740
  * @param options Optional request options.
629
741
  */
630
- getOne(id: string, options?: ReadOptions<T> & RequestOptions): Promise<T | null>;
742
+ getOne<E extends string = never>(id: string, options?: ReadOptions<TFields, E> & RequestOptions): Promise<(T & {
743
+ expand?: ExpandObj<E>;
744
+ }) | null>;
631
745
  /**
632
746
  * Create a new record.
633
747
  * @param data Record fields. When `T` is a concrete shape (e.g. a generated
@@ -680,7 +794,7 @@ declare class CollectionService<T = ApiRecord, TData = never> {
680
794
  * `types.schemas`). Enables schema-aware defaults: hidden fields are
681
795
  * excluded from responses, select/expand are validated.
682
796
  */
683
- withSchema(schema: CollectionSchema): CollectionService<T, TData>;
797
+ withSchema(schema: CollectionSchema): CollectionService<T, TData, TFields>;
684
798
  /**
685
799
  * Subscribe to realtime changes for this collection.
686
800
  * The event's `action` is one of `"create" | "update" | "delete"` and the