@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 +1 -1
- package/src/admin.ts +90 -8
- package/src/collection.ts +12 -8
- package/src/types.ts +4 -2
package/package.json
CHANGED
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 (
|
|
656
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
168
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
170
|
-
//
|
|
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
|
} & {
|