@bunbase-ae/js 2.13.3-next.300.4a69028 → 2.13.3-next.310.9b923b7

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.300.4a69028",
3
+ "version": "2.13.3-next.310.9b923b7",
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",
@@ -1097,11 +1099,27 @@ export interface NamedQueryParamDef {
1097
1099
  required?: boolean;
1098
1100
  }
1099
1101
 
1102
+ /**
1103
+ * Access rules valid for a named query.
1104
+ *
1105
+ * Named queries don't carry a record-level owner — `"owner"` (which is valid
1106
+ * on collections) has no per-row meaning here and is rejected at create/update
1107
+ * time. Use `"authenticated"` for logged-in-only access, or `{ role: "..." }`
1108
+ * for role-gated access.
1109
+ */
1110
+ export type NamedQueryAccessRule = Exclude<AccessRule, "owner">;
1111
+
1100
1112
  export interface NamedQuery {
1101
1113
  name: string;
1102
1114
  description: string | null;
1103
1115
  sql: string;
1104
1116
  params: Record<string, NamedQueryParamDef>;
1117
+ /**
1118
+ * The persisted access rule. New queries are restricted to
1119
+ * {@link NamedQueryAccessRule}; this remains the broader {@link AccessRule}
1120
+ * union so legacy rows persisted before the validator was tightened can
1121
+ * still be read.
1122
+ */
1105
1123
  access: AccessRule;
1106
1124
  row_limit: number;
1107
1125
  timeout_ms: number;
@@ -1122,7 +1140,7 @@ export interface CreateNamedQueryInput {
1122
1140
  description?: string | null;
1123
1141
  sql: string;
1124
1142
  params?: Record<string, NamedQueryParamDef>;
1125
- access?: AccessRule;
1143
+ access?: NamedQueryAccessRule;
1126
1144
  row_limit?: number;
1127
1145
  timeout_ms?: number;
1128
1146
  result_cache_ttl_s?: number;
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
  } & {