@bunbase-ae/js 1.2.2-next.45.452b4e1 → 1.3.1-next.46.c15bda5
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 +1 -1
- package/src/admin.ts +85 -15
- package/src/collection.ts +20 -10
- package/src/index.ts +4 -0
- package/src/types.ts +80 -5
package/package.json
CHANGED
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
555
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
582
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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?:
|
|
102
|
+
group_by?: TGroupBy;
|
|
98
103
|
filter?: Filter<T>;
|
|
99
104
|
include_deleted?: boolean;
|
|
100
105
|
} = {},
|
|
101
|
-
): Promise<
|
|
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<
|
|
111
|
-
|
|
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
|
|
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
|
|
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}]`] =
|
|
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]
|
|
166
|
+
[K in Exclude<keyof T, keyof BunBaseRecord>]?: FilterFieldValue<T[K]>;
|
|
102
167
|
} & {
|
|
103
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
|