@bunbase-ae/js 2.13.3-next.288.cbcf81c → 2.13.3-next.304.484953e

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": "2.13.3-next.288.cbcf81c",
3
+ "version": "2.13.3-next.304.484953e",
4
4
  "type": "module",
5
5
  "description": "TypeScript/JavaScript SDK for BunBase",
6
6
  "license": "UNLICENSED",
package/src/admin.ts CHANGED
@@ -650,23 +650,25 @@ class AdminCollectionsClient {
650
650
  if (opts.includeDeleted) params.include_deleted = "true";
651
651
  if (opts.search) params.search = opts.search;
652
652
  if (opts.filter) {
653
+ // Encode filters as a single ?filter=[{field,op,value}, …] JSON array.
654
+ // See buildQueryString in collection.ts for the rationale (issue #533):
655
+ // the bracket form silently corrupts values containing commas.
656
+ const fieldFilters: { field: string; op: string; value: unknown }[] = [];
653
657
  for (const [key, value] of Object.entries(opts.filter)) {
654
658
  if (value === undefined || value === null) continue;
655
- if (typeof value === "object" && !Array.isArray(value)) {
656
- // Operator filter: { age: { gte: 18 } } → filter[age][gte]=18
659
+ if (Array.isArray(value)) {
660
+ fieldFilters.push({ field: key, op: "in", value });
661
+ } else if (typeof value === "object") {
657
662
  for (const [op, opVal] of Object.entries(value as Record<string, unknown>)) {
658
663
  if (opVal !== undefined && opVal !== null) {
659
- params[`filter[${key}][${op}]`] = String(opVal);
664
+ fieldFilters.push({ field: key, op, value: opVal });
660
665
  }
661
666
  }
662
- } else if (Array.isArray(value)) {
663
- // Array shorthand: { stage: ["a", "b"] } → filter[stage][in]=a,b
664
- params[`filter[${key}][in]`] = value.join(",");
665
667
  } else {
666
- // Equality filter: { status: "active" } → filter[status]=active
667
- params[`filter[${key}]`] = String(value);
668
+ fieldFilters.push({ field: key, op: "eq", value });
668
669
  }
669
670
  }
671
+ if (fieldFilters.length) params.filter = JSON.stringify(fieldFilters);
670
672
  }
671
673
  return this.http.request<AdminListResult<T & AdminRecord>>(
672
674
  "GET",
@@ -1179,6 +1181,84 @@ class AdminNamedQueriesClient {
1179
1181
  }
1180
1182
  }
1181
1183
 
1184
+ // ─── Write hooks ──────────────────────────────────────────────────────────────
1185
+
1186
+ export type HookEvent =
1187
+ | "beforeCreate"
1188
+ | "afterCreate"
1189
+ | "beforeUpdate"
1190
+ | "afterUpdate"
1191
+ | "beforeDelete"
1192
+ | "afterDelete";
1193
+
1194
+ export interface HookConfig {
1195
+ id: string;
1196
+ collection: string;
1197
+ event: HookEvent;
1198
+ handler_url: string;
1199
+ /** True when a signing secret is configured. Secret value is write-only. */
1200
+ has_handler_secret: boolean;
1201
+ timeout_ms: number;
1202
+ enabled: boolean;
1203
+ sort_order: number;
1204
+ created_at: number;
1205
+ }
1206
+
1207
+ export interface CreateHookInput {
1208
+ collection: string;
1209
+ event: HookEvent;
1210
+ handler_url: string;
1211
+ /** Optional HMAC-SHA256 signing secret. Write-only — never returned. */
1212
+ handler_secret?: string | null;
1213
+ /** Per-call timeout. Default 5000. */
1214
+ timeout_ms?: number;
1215
+ /** When false, the hook is skipped at delivery time. Default true. */
1216
+ enabled?: boolean;
1217
+ /** Ascending order for hooks sharing the same (collection, event). Default 0. */
1218
+ sort_order?: number;
1219
+ }
1220
+
1221
+ export type UpdateHookInput = Partial<Omit<CreateHookInput, "collection" | "event">>;
1222
+
1223
+ class AdminHooksClient {
1224
+ constructor(private readonly http: HttpClient) {}
1225
+
1226
+ async list(opts?: { collection?: string }): Promise<HookConfig[]> {
1227
+ const params: Record<string, string> = {};
1228
+ if (opts?.collection) params.collection = opts.collection;
1229
+ const res = await this.http.request<{ items: HookConfig[] }>("GET", "/api/v1/admin/hooks", {
1230
+ query: params,
1231
+ });
1232
+ return res.items;
1233
+ }
1234
+
1235
+ async get(id: string): Promise<HookConfig | null> {
1236
+ try {
1237
+ return await this.http.request<HookConfig>(
1238
+ "GET",
1239
+ `/api/v1/admin/hooks/${encodeURIComponent(id)}`,
1240
+ );
1241
+ } catch (err) {
1242
+ if (err instanceof BunBaseError && err.status === 404) return null;
1243
+ throw err;
1244
+ }
1245
+ }
1246
+
1247
+ async create(input: CreateHookInput): Promise<HookConfig> {
1248
+ return this.http.request<HookConfig>("POST", "/api/v1/admin/hooks", { body: input });
1249
+ }
1250
+
1251
+ async update(id: string, patch: UpdateHookInput): Promise<HookConfig> {
1252
+ return this.http.request<HookConfig>("PATCH", `/api/v1/admin/hooks/${encodeURIComponent(id)}`, {
1253
+ body: patch,
1254
+ });
1255
+ }
1256
+
1257
+ async delete(id: string): Promise<void> {
1258
+ await this.http.request("DELETE", `/api/v1/admin/hooks/${encodeURIComponent(id)}`);
1259
+ }
1260
+ }
1261
+
1182
1262
  // Tenant membership management — surfaces the /api/v1/admin/tenants/* endpoints
1183
1263
  // added in v2.5.2 (#277/#323) so operators don't need to hand-roll fetch calls
1184
1264
  // to onboard a tenant member.
@@ -1243,6 +1323,7 @@ export class AdminClient {
1243
1323
  readonly backups: AdminBackupsClient;
1244
1324
  readonly migrations: AdminMigrationsClient;
1245
1325
  readonly queries: AdminNamedQueriesClient;
1326
+ readonly hooks: AdminHooksClient;
1246
1327
  readonly logs: AdminLogsClient;
1247
1328
  readonly system: AdminSystemClient;
1248
1329
  readonly tenants: AdminTenantsClient;
@@ -1257,6 +1338,7 @@ export class AdminClient {
1257
1338
  this.backups = new AdminBackupsClient(http);
1258
1339
  this.migrations = new AdminMigrationsClient(http);
1259
1340
  this.queries = new AdminNamedQueriesClient(http);
1341
+ this.hooks = new AdminHooksClient(http);
1260
1342
  this.logs = new AdminLogsClient(http);
1261
1343
  this.system = new AdminSystemClient(http);
1262
1344
  this.tenants = new AdminTenantsClient(http);
package/src/collection.ts CHANGED
@@ -161,24 +161,28 @@ export function buildQueryString<T extends Record<string, unknown> = Record<stri
161
161
  const params: Record<string, string> = {};
162
162
 
163
163
  if (query.filter) {
164
+ // Encode filters as a single ?filter=[{field,op,value}, …] JSON array.
165
+ // The bracket form (?filter[field][op]=value) joins array values on ",", which
166
+ // silently corrupts any value containing a comma — see issue #533. The JSON
167
+ // form preserves array values verbatim and is parsed by the server's
168
+ // parseFilterJson (apps/server/src/routes/_filterParams.ts).
169
+ const fieldFilters: { field: string; op: string; value: unknown }[] = [];
164
170
  for (const [field, value] of Object.entries(query.filter)) {
165
171
  if (value === undefined || value === null) continue;
166
172
 
167
- if (typeof value === "object" && !Array.isArray(value)) {
168
- // Operator filter: { age: { gte: 18 } } → filter[age][gte]=18
173
+ if (Array.isArray(value)) {
174
+ fieldFilters.push({ field, op: "in", value });
175
+ } else if (typeof value === "object") {
169
176
  for (const [op, opVal] of Object.entries(value as Record<string, unknown>)) {
170
177
  if (opVal !== undefined && opVal !== null) {
171
- params[`filter[${field}][${op}]`] = String(opVal);
178
+ fieldFilters.push({ field, op, value: opVal });
172
179
  }
173
180
  }
174
- } else if (Array.isArray(value)) {
175
- // Array shorthand: { stage: ["a", "b"] } → filter[stage][in]=a,b
176
- params[`filter[${field}][in]`] = value.join(",");
177
181
  } else {
178
- // Equality filter: { status: "published" } → filter[status]=published
179
- params[`filter[${field}]`] = String(value);
182
+ fieldFilters.push({ field, op: "eq", value });
180
183
  }
181
184
  }
185
+ if (fieldFilters.length) params.filter = JSON.stringify(fieldFilters);
182
186
  }
183
187
 
184
188
  if (query.sort) params.sort = query.sort;
package/src/types.ts CHANGED
@@ -166,8 +166,10 @@ export type FilterFieldValue<V> = V | FilterArrayShorthandValue<V> | FilterOpera
166
166
  * filter: { rfid_tag: { in: ["a", "b"] } }
167
167
  * filter: { score: { gte: 0, lte: 100 } }
168
168
  */
169
- // Simple: { status: "published" } → filter[status]=published
170
- // Operator: { age: { gte: 18 } } → filter[age][gte]=18
169
+ // Wire format: a single ?filter=[{field,op,value}, …] JSON entry.
170
+ // Simple { status: "published" } → [{ field: "status", op: "eq", value: "published" }]
171
+ // Operator { age: { gte: 18 } } → [{ field: "age", op: "gte", value: 18 }]
172
+ // Array { id: ["a", "b"] } → [{ field: "id", op: "in", value: ["a", "b"] }]
171
173
  export type Filter<T = Record<string, unknown>> = {
172
174
  [K in Exclude<keyof T, keyof BunBaseRecord>]?: FilterFieldValue<T[K]>;
173
175
  } & {