@open-mercato/shared 0.7.1-develop.7102.1.b41f7e3e51 → 0.7.1-develop.7103.1.41ff100d93

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.
@@ -1,2 +1,2 @@
1
- [build:shared] found 267 entry points
1
+ [build:shared] found 268 entry points
2
2
  [build:shared] built successfully
package/AGENTS.md CHANGED
@@ -253,6 +253,14 @@ MUST rules:
253
253
  - `message` is passed through verbatim and is never derived from an entity name — keep routing 404 copy through `translate(...)` so it stays translatable.
254
254
  - `assertFound` treats every falsy value as missing. Use it for entity/object lookups only, never to guard numbers or strings where `0`/`''` are valid results.
255
255
 
256
+ ### Query Parameter Parsing — MUST NOT rebuild the query object with `Object.fromEntries`
257
+
258
+ ```typescript
259
+ import { buildQueryParams, readQueryParamList, toQueryValueList } from '@open-mercato/shared/lib/crud/query-params'
260
+ ```
261
+
262
+ `Object.fromEntries(url.searchParams.entries())` keeps only the LAST value of a repeated key, so `?status=win&status=loose` silently becomes `'loose'` (#5548). `buildQueryParams(url.searchParams)` groups instead: a key seen once stays a string, a key seen twice or more becomes `string[]`. It never splits on commas, so `?ids=a,b` and free-text filters keep their literal value. Where a field's contract says a comma separates values, use `readQueryParamList(searchParams, key)` (or `toQueryValueList(raw)` on an already-parsed value) — they treat the repeated and comma forms as equivalent. MUST use these instead of a per-route `getAll` + split + trim copy.
263
+
256
264
  ### CRUD Multi-ID Filtering
257
265
 
258
266
  - Use `parseIdsParam()` and `mergeIdFilter()` from `@open-mercato/shared/lib/crud/ids` for factory-level `ids` query support.
@@ -48,6 +48,7 @@ import { getTranslationOverlayPlugin } from "@open-mercato/shared/lib/localizati
48
48
  import { applyResponseEnrichers, applyResponseEnricherToRecord, resolveListCacheEnricherPlan } from "./enricher-runner.js";
49
49
  import { runApiInterceptorsAfter, runApiInterceptorsBefore } from "./interceptor-runner.js";
50
50
  import { mergeIdFilter, parseIdsParam, isIdsParamProvided } from "./ids.js";
51
+ import { buildQueryParams } from "./query-params.js";
51
52
  import { mergeAdvancedFilters } from "./advanced-filter-integration.js";
52
53
  import { parseExtensionHeaders } from "../umes/extension-headers.js";
53
54
  import { createGenericOptimisticLockReader } from "./optimistic-lock.js";
@@ -991,7 +992,7 @@ function makeCrudRoute(opts) {
991
992
  return json({ error: "Not implemented" }, { status: 501 });
992
993
  }
993
994
  const url = new URL(request.url);
994
- const rawQueryParams = Object.fromEntries(url.searchParams.entries());
995
+ const rawQueryParams = buildQueryParams(url.searchParams);
995
996
  profiler.mark("query_parsed");
996
997
  let validated = opts.list.schema.parse(rawQueryParams);
997
998
  profiler.mark("query_validated");
@@ -2202,7 +2203,7 @@ function makeCrudRoute(opts) {
2202
2203
  if (useCommand) {
2203
2204
  const action = opts.actions.delete;
2204
2205
  const body = await request.json().catch(() => ({}));
2205
- const raw = { body, query: Object.fromEntries(url.searchParams.entries()) };
2206
+ const raw = { body, query: buildQueryParams(url.searchParams) };
2206
2207
  const parsed = action.schema ? action.schema.parse(raw) : raw;
2207
2208
  const interceptorInput = parsed && typeof parsed === "object" && parsed.body && typeof parsed.body === "object" ? parsed.body : body;
2208
2209
  const beforeInterceptors2 = await applyInterceptorsBefore({
@@ -2218,7 +2219,7 @@ function makeCrudRoute(opts) {
2218
2219
  const interceptedBody = interceptorRequestPayload.body ?? {};
2219
2220
  const reparsedRaw = {
2220
2221
  body: interceptedBody,
2221
- query: Object.fromEntries(url.searchParams.entries())
2222
+ query: buildQueryParams(url.searchParams)
2222
2223
  };
2223
2224
  const reparsed = action.schema ? action.schema.parse(reparsedRaw) : reparsedRaw;
2224
2225
  const input = action.mapInput ? await action.mapInput({ parsed: reparsed, raw: reparsedRaw, ctx }) : reparsed;
@@ -2321,7 +2322,7 @@ function makeCrudRoute(opts) {
2321
2322
  request,
2322
2323
  method: "DELETE",
2323
2324
  body: idFrom === "query" ? void 0 : { id },
2324
- query: idFrom === "query" ? Object.fromEntries(url.searchParams.entries()) : void 0
2325
+ query: idFrom === "query" ? buildQueryParams(url.searchParams) : void 0
2325
2326
  });
2326
2327
  if (beforeInterceptors.errorResponse) return beforeInterceptors.errorResponse;
2327
2328
  interceptorRequestPayload = beforeInterceptors.requestPayload;