@bunbase-ae/js 1.2.2-next.45.452b4e1 → 1.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bunbase-ae/js",
3
- "version": "1.2.2-next.45.452b4e1",
3
+ "version": "1.3.0",
4
4
  "type": "module",
5
5
  "description": "TypeScript/JavaScript SDK for BunBase",
6
6
  "license": "UNLICENSED",
package/src/admin.ts CHANGED
@@ -14,6 +14,7 @@
14
14
  // client.admin.system — health + stats
15
15
 
16
16
  import type { HttpClient } from "./http";
17
+ import type { Filter } from "./types";
17
18
 
18
19
  // ─── Shared admin types ───────────────────────────────────────────────────────
19
20
 
@@ -507,7 +508,16 @@ class AdminCollectionsClient {
507
508
 
508
509
  // ── Records ─────────────────────────────────────────────────────────────────
509
510
 
510
- async listRecords(
511
+ /**
512
+ * List records from a collection with optional filtering, sorting, and pagination.
513
+ *
514
+ * Pass a type parameter to get typed records:
515
+ * ```ts
516
+ * const data = await bbClient.admin.collections.listRecords<Device>("devices", { ... });
517
+ * // data.items is (Device & AdminRecord)[]
518
+ * ```
519
+ */
520
+ async listRecords<T extends Record<string, unknown> = Record<string, unknown>>(
511
521
  collection: string,
512
522
  opts: {
513
523
  /** @deprecated Use `after` for keyset pagination. */
@@ -515,11 +525,12 @@ class AdminCollectionsClient {
515
525
  limit?: number;
516
526
  after?: string;
517
527
  sort?: string;
518
- filter?: Record<string, string>;
528
+ /** Typed filter supporting equality and operator filters ({ gte, lte, in, like, ne, eq }). */
529
+ filter?: Filter<T>;
519
530
  includeDeleted?: boolean;
520
531
  search?: string;
521
532
  } = {},
522
- ): Promise<AdminListResult<AdminRecord>> {
533
+ ): Promise<AdminListResult<T & AdminRecord>> {
523
534
  const params: Record<string, string> = {
524
535
  limit: String(opts.limit ?? 50),
525
536
  };
@@ -530,19 +541,45 @@ class AdminCollectionsClient {
530
541
  if (opts.search) params.search = opts.search;
531
542
  if (opts.filter) {
532
543
  for (const [key, value] of Object.entries(opts.filter)) {
533
- params[`filter[${key}]`] = value;
544
+ if (value === undefined || value === null) continue;
545
+ if (typeof value === "object" && !Array.isArray(value)) {
546
+ // Operator filter: { age: { gte: 18 } } → filter[age][gte]=18
547
+ for (const [op, opVal] of Object.entries(value as Record<string, unknown>)) {
548
+ if (opVal !== undefined && opVal !== null) {
549
+ params[`filter[${key}][${op}]`] = String(opVal);
550
+ }
551
+ }
552
+ } else if (Array.isArray(value)) {
553
+ // Array shorthand: { stage: ["a", "b"] } → filter[stage][in]=a,b
554
+ params[`filter[${key}][in]`] = value.join(",");
555
+ } else {
556
+ // Equality filter: { status: "active" } → filter[status]=active
557
+ params[`filter[${key}]`] = String(value);
558
+ }
534
559
  }
535
560
  }
536
- return this.http.request<AdminListResult<AdminRecord>>(
561
+ return this.http.request<AdminListResult<T & AdminRecord>>(
537
562
  "GET",
538
563
  `/api/v1/admin/collections/${collection}/records`,
539
564
  { query: params },
540
565
  );
541
566
  }
542
567
 
543
- async getRecord(collection: string, id: string): Promise<AdminRecord | null> {
568
+ /**
569
+ * Fetch a single record by ID, returning null when not found.
570
+ *
571
+ * Pass a type parameter to get a typed record:
572
+ * ```ts
573
+ * const rec = await bbClient.admin.collections.getRecord<PosOrderItem>("pos_order_items", id);
574
+ * // rec is (PosOrderItem & AdminRecord) | null
575
+ * ```
576
+ */
577
+ async getRecord<T extends Record<string, unknown> = Record<string, unknown>>(
578
+ collection: string,
579
+ id: string,
580
+ ): Promise<(T & AdminRecord) | null> {
544
581
  try {
545
- return await this.http.request<AdminRecord>(
582
+ return await this.http.request<T & AdminRecord>(
546
583
  "GET",
547
584
  `/api/v1/admin/collections/${collection}/records/${id}`,
548
585
  );
@@ -551,20 +588,41 @@ class AdminCollectionsClient {
551
588
  }
552
589
  }
553
590
 
554
- async createRecord(collection: string, data: Record<string, unknown>): Promise<AdminRecord> {
555
- return this.http.request<AdminRecord>(
591
+ /**
592
+ * Create a new record in a collection.
593
+ *
594
+ * Pass a type parameter to get a typed result:
595
+ * ```ts
596
+ * const rec = await bbClient.admin.collections.createRecord<Device>("devices", data);
597
+ * // rec is Device & AdminRecord
598
+ * ```
599
+ */
600
+ async createRecord<T extends Record<string, unknown> = Record<string, unknown>>(
601
+ collection: string,
602
+ data: Partial<T>,
603
+ ): Promise<T & AdminRecord> {
604
+ return this.http.request<T & AdminRecord>(
556
605
  "POST",
557
606
  `/api/v1/admin/collections/${collection}/records`,
558
607
  { body: data },
559
608
  );
560
609
  }
561
610
 
562
- async updateRecord(
611
+ /**
612
+ * Update a record by ID.
613
+ *
614
+ * Pass a type parameter to get a typed result:
615
+ * ```ts
616
+ * const rec = await bbClient.admin.collections.updateRecord<Device>("devices", id, patch);
617
+ * // rec is Device & AdminRecord
618
+ * ```
619
+ */
620
+ async updateRecord<T extends Record<string, unknown> = Record<string, unknown>>(
563
621
  collection: string,
564
622
  id: string,
565
- patch: Record<string, unknown>,
566
- ): Promise<AdminRecord> {
567
- return this.http.request<AdminRecord>(
623
+ patch: Partial<T>,
624
+ ): Promise<T & AdminRecord> {
625
+ return this.http.request<T & AdminRecord>(
568
626
  "PATCH",
569
627
  `/api/v1/admin/collections/${collection}/records/${id}`,
570
628
  { body: patch },
@@ -578,8 +636,20 @@ class AdminCollectionsClient {
578
636
  );
579
637
  }
580
638
 
581
- async restoreRecord(collection: string, id: string): Promise<AdminRecord> {
582
- return this.http.request<AdminRecord>(
639
+ /**
640
+ * Restore a soft-deleted record.
641
+ *
642
+ * Pass a type parameter to get a typed result:
643
+ * ```ts
644
+ * const rec = await bbClient.admin.collections.restoreRecord<Device>("devices", id);
645
+ * // rec is Device & AdminRecord
646
+ * ```
647
+ */
648
+ async restoreRecord<T extends Record<string, unknown> = Record<string, unknown>>(
649
+ collection: string,
650
+ id: string,
651
+ ): Promise<T & AdminRecord> {
652
+ return this.http.request<T & AdminRecord>(
583
653
  "POST",
584
654
  `/api/v1/admin/collections/${collection}/records/${id}/restore`,
585
655
  { body: {} },
package/src/collection.ts CHANGED
@@ -8,7 +8,8 @@
8
8
  import type { HttpClient } from "./http";
9
9
  import type {
10
10
  AggregateFunction,
11
- AggregateResult,
11
+ AggregateGroupedResult,
12
+ AggregateScalarResult,
12
13
  BatchOperation,
13
14
  BatchResult,
14
15
  BunBaseRecord,
@@ -90,15 +91,19 @@ export class CollectionClient<T extends Record<string, unknown> = Record<string,
90
91
  }
91
92
 
92
93
  // Compute an aggregate (sum, avg, min, max, count) over the collection.
93
- async aggregate(
94
+ //
95
+ // The return type is discriminated by whether group_by is provided:
96
+ // without group_by → { value: number | null }
97
+ // with group_by → { groups: Array<{ group: unknown; value: number | null }> }
98
+ async aggregate<TGroupBy extends string | undefined = undefined>(
94
99
  fn: AggregateFunction,
95
100
  options: {
96
101
  field?: string;
97
- group_by?: string;
102
+ group_by?: TGroupBy;
98
103
  filter?: Filter<T>;
99
104
  include_deleted?: boolean;
100
105
  } = {},
101
- ): Promise<AggregateResult> {
106
+ ): Promise<[TGroupBy] extends [string] ? AggregateGroupedResult : AggregateScalarResult> {
102
107
  const qs: Record<string, string> = { fn };
103
108
  if (options.field) qs.field = options.field;
104
109
  if (options.group_by) qs.group_by = options.group_by;
@@ -107,9 +112,9 @@ export class CollectionClient<T extends Record<string, unknown> = Record<string,
107
112
  const filterQs = buildQueryString({ filter: options.filter });
108
113
  Object.assign(qs, filterQs);
109
114
  }
110
- return this.http.request<AggregateResult>("GET", `/api/v1/${this.name}/aggregate`, {
111
- query: qs,
112
- });
115
+ return this.http.request<
116
+ [TGroupBy] extends [string] ? AggregateGroupedResult : AggregateScalarResult
117
+ >("GET", `/api/v1/${this.name}/aggregate`, { query: qs });
113
118
  }
114
119
 
115
120
  // Count records matching the query filters.
@@ -150,7 +155,9 @@ export class CollectionClient<T extends Record<string, unknown> = Record<string,
150
155
 
151
156
  // ─── Query string builder ──────────────────────────────────────────────────────
152
157
 
153
- export function buildQueryString<T>(query: ListQuery<T>): Record<string, string> {
158
+ export function buildQueryString<T extends Record<string, unknown> = Record<string, unknown>>(
159
+ query: ListQuery<T>,
160
+ ): Record<string, string> {
154
161
  const params: Record<string, string> = {};
155
162
 
156
163
  if (query.filter) {
@@ -159,14 +166,17 @@ export function buildQueryString<T>(query: ListQuery<T>): Record<string, string>
159
166
 
160
167
  if (typeof value === "object" && !Array.isArray(value)) {
161
168
  // Operator filter: { age: { gte: 18 } } → filter[age][gte]=18
162
- for (const [op, opVal] of Object.entries(value as Filter)) {
169
+ for (const [op, opVal] of Object.entries(value as Record<string, unknown>)) {
163
170
  if (opVal !== undefined && opVal !== null) {
164
171
  params[`filter[${field}][${op}]`] = String(opVal);
165
172
  }
166
173
  }
174
+ } else if (Array.isArray(value)) {
175
+ // Array shorthand: { stage: ["a", "b"] } → filter[stage][in]=a,b
176
+ params[`filter[${field}][in]`] = value.join(",");
167
177
  } else {
168
178
  // Equality filter: { status: "published" } → filter[status]=published
169
- params[`filter[${field}]`] = Array.isArray(value) ? value.join(",") : String(value);
179
+ params[`filter[${field}]`] = String(value);
170
180
  }
171
181
  }
172
182
  }
package/src/index.ts CHANGED
@@ -41,7 +41,9 @@ export { RealtimeClient, type SubscribeOptions } from "./realtime";
41
41
  export { type SignedUploadResult, StorageClient, type UploadOptions } from "./storage";
42
42
  export {
43
43
  type AggregateFunction,
44
+ type AggregateGroupedResult,
44
45
  type AggregateResult,
46
+ type AggregateScalarResult,
45
47
  type ApiKey,
46
48
  type AuthResult,
47
49
  type AuthUser,
@@ -60,7 +62,9 @@ export {
60
62
  type FieldRule,
61
63
  type FileRecord,
62
64
  type Filter,
65
+ type FilterFieldValue,
63
66
  type FilterOperator,
67
+ type FilterOperatorValue,
64
68
  type FilterValue,
65
69
  type GetQuery,
66
70
  type ListQuery,
package/src/types.ts CHANGED
@@ -89,18 +89,88 @@ export interface FileRecord {
89
89
 
90
90
  export type FilterOperator = "eq" | "ne" | "gt" | "lt" | "gte" | "lte" | "like" | "in";
91
91
 
92
+ /** Scalar filter value for plain equality filters. @deprecated Use FilterFieldValue<V> for typed filters. */
92
93
  export type FilterValue = string | number | boolean | string[] | number[];
93
94
 
95
+ /** @deprecated Use FilterOperatorValue<V> for typed operator filters. */
94
96
  export interface FieldFilter {
95
97
  [op: string]: FilterValue;
96
98
  }
97
99
 
100
+ /**
101
+ * Typed operator object for a single field.
102
+ * Constrains each operator's value to the field's own type V.
103
+ *
104
+ * @example
105
+ * // Numeric field: { gte: 18, lte: 65 }
106
+ * // String field: { in: ["draft", "published"] }
107
+ * // Any field: { ne: null }
108
+ */
109
+ export type FilterOperatorValue<V> = {
110
+ eq?: V;
111
+ ne?: V;
112
+ gt?: V;
113
+ lt?: V;
114
+ gte?: V;
115
+ lte?: V;
116
+ like?: string;
117
+ in?: V[];
118
+ };
119
+
120
+ /**
121
+ * Array shorthand for "in" filters.
122
+ *
123
+ * This preserves the existing SDK ergonomics:
124
+ * `{ stage: ["draft", "published"] }`
125
+ *
126
+ * The shorthand is only meaningful for scalar string/number-like fields.
127
+ */
128
+ export type FilterArrayShorthandValue<V> =
129
+ Exclude<V, null | undefined> extends string
130
+ ? string[]
131
+ : Exclude<V, null | undefined> extends number
132
+ ? number[]
133
+ : never;
134
+
135
+ /**
136
+ * A filter field accepts either a plain value (equality) or an operator object.
137
+ *
138
+ * @example
139
+ * // Equality: "published"
140
+ * // Operator: { gte: 18 }
141
+ * // Array-in: { in: ["a", "b"] }
142
+ */
143
+ export type FilterFieldValue<V> = V | FilterArrayShorthandValue<V> | FilterOperatorValue<V>;
144
+
145
+ /**
146
+ * Filter over collection records of type T.
147
+ *
148
+ * Supports both equality filters `{ stage: "chopping" }` and operator
149
+ * filters `{ _created_at: { gte: timestamp }, rfid_tag: { in: ["a","b"] } }`.
150
+ *
151
+ * BunBaseRecord system fields (_id, _created_at, _updated_at, _owner_id,
152
+ * _deleted_at) are always available for filtering regardless of T.
153
+ *
154
+ * @example
155
+ * // Simple equality
156
+ * filter: { stage: "chopping" }
157
+ *
158
+ * // Operator filters — no cast required
159
+ * filter: { _created_at: { gte: timestamp } }
160
+ * filter: { rfid_tag: { in: ["a", "b"] } }
161
+ * filter: { score: { gte: 0, lte: 100 } }
162
+ */
98
163
  // Simple: { status: "published" } → filter[status]=published
99
164
  // Operator: { age: { gte: 18 } } → filter[age][gte]=18
100
165
  export type Filter<T = Record<string, unknown>> = {
101
- [K in keyof T]?: T[K] | FieldFilter;
166
+ [K in Exclude<keyof T, keyof BunBaseRecord>]?: FilterFieldValue<T[K]>;
102
167
  } & {
103
- [key: string]: FilterValue | FieldFilter | undefined;
168
+ // BunBaseRecord system fields always filterable, typed to their canonical types
169
+ _id?: FilterFieldValue<string>;
170
+ _created_at?: FilterFieldValue<number>;
171
+ _updated_at?: FilterFieldValue<number>;
172
+ _owner_id?: FilterFieldValue<string | null>;
173
+ _deleted_at?: FilterFieldValue<number | null | undefined>;
104
174
  };
105
175
 
106
176
  export interface ListQuery<T = Record<string, unknown>> {
@@ -193,9 +263,14 @@ export interface FieldRule {
193
263
 
194
264
  export type AggregateFunction = "sum" | "avg" | "min" | "max" | "count";
195
265
 
196
- export type AggregateResult =
197
- | { value: number | null }
198
- | { groups: Array<{ group: unknown; value: number | null }> };
266
+ /** Result shape when no group_by is provided. */
267
+ export type AggregateScalarResult = { value: number | null };
268
+
269
+ /** Result shape when group_by is provided. */
270
+ export type AggregateGroupedResult = { groups: Array<{ group: unknown; value: number | null }> };
271
+
272
+ /** Union of all possible aggregate result shapes. Narrow via `'groups' in result`. */
273
+ export type AggregateResult = AggregateScalarResult | AggregateGroupedResult;
199
274
 
200
275
  // ─── Realtime types ───────────────────────────────────────────────────────────
201
276