{"record":{"id":"ff13b03d2d35eda4","repo":"Budibase/budibase","slug":"view-is-missing-queryui-field","errorCode":null,"errorMessage":"view is missing queryUI field","messagePattern":"view is missing queryUI field","errorType":"validation","errorClass":"HTTPError","httpStatus":400,"severity":"error","filePath":"packages/server/src/sdk/workspace/views/utils.ts","lineNumber":38,"sourceCode":"}\n\nexport function ensureQueryUISet(viewArg: Readonly<ViewV2>): ViewV2 {\n  const view = cloneDeep<ViewV2>(viewArg)\n  if (!view.queryUI && view.query && !isEmptyObject(view.query)) {\n    if (!Array.isArray(view.query)) {\n      // In practice this should not happen. `view.query`, at the time this code\n      // goes into the codebase, only contains LegacyFilter[] in production.\n      // We're changing it in the change that this comment is part of to also\n      // include SearchFilters objects. These are created when we receive an\n      // update to a ViewV2 that contains a queryUI and not a query field. We\n      // can convert UISearchFilter (the type of queryUI) to SearchFilters,\n      // but not LegacyFilter[], they are incompatible due to UISearchFilter\n      // and SearchFilters being recursive types.\n      //\n      // So despite the type saying that `view.query` is a LegacyFilter[] |\n      // SearchFilters, it will never be a SearchFilters when a `view.queryUI`\n      // is specified, making it \"safe\" to throw an error here.\n      throw new HTTPError(\"view is missing queryUI field\", 400)\n    }\n\n    view.queryUI = utils.processSearchFilters(view.query)\n  }\n  return view\n}\n\nexport function ensureQuerySet(viewArg: Readonly<ViewV2>): ViewV2 {\n  const view = cloneDeep<ViewV2>(viewArg)\n  // We consider queryUI to be the source of truth, so we don't check for the\n  // presence of query here. We will overwrite it regardless of whether it is\n  // present or not.\n  if (view.queryUI && !isEmptyObject(view.queryUI)) {\n    view.query = dataFilters.buildQuery(view.queryUI)\n  }\n  return view\n}\n","sourceCodeStart":20,"sourceCodeEnd":56,"githubUrl":"https://github.com/Budibase/budibase/blob/a81a902e9a8fe55b467d106765f6638f12e35c49/packages/server/src/sdk/workspace/views/utils.ts#L20-L56","documentation":"ensureQueryUISet normalizes a ViewV2 so that its `queryUI` field is populated from `query`. Views persist filters as either LegacyFilter[] (array form) or SearchFilters (object form). The code can only convert the legacy array form to queryUI; if `query` is a non-empty object (SearchFilters) but `queryUI` is missing, there is no safe conversion, so it throws a 400 HTTPError.","triggerScenarios":"Any of processTable, get, getEnriched, create, update, or enrichedViews loads/accepts a view whose `query` is a non-empty SearchFilters object while `queryUI` is undefined/null. This happens when a view doc was persisted with SearchFilters-shaped query data but without the queryUI field (e.g. written by code that bypassed ensureQuerySet, hand-edited docs, or docs from a version before queryUI existed).","commonSituations":"Upgrading Budibase and hitting views migrated from older versions; importing/exporting apps where view docs were edited externally; writing directly to CouchDB or via scripts that set `query` without `queryUI`; partial API calls that sent `query` instead of `queryUI` on view create/update.","solutions":["Check the offending view doc in the workspace DB and either add a `queryUI` field (UISearchFilter form) or convert `query` back to the LegacyFilter[] array form","Re-save the view through the builder UI so the server writes both `query` and `queryUI` via ensureQuerySet","If programmatically creating views via the API, send `queryUI` (not a SearchFilters-shaped `query`)","Delete and recreate the corrupted view if its filters are not needed"],"exampleFix":"// before (API payload that triggers the error)\n{ \"name\": \"MyView\", \"query\": { \"equal\": { \"status\": \"live\" } } }\n// after\n{ \"name\": \"MyView\", \"queryUI\": { \"onGroups\": [{ \"operator\": \"equal\", \"field\": \"status\", \"value\": \"live\" }] } }","handlingStrategy":"validation","validationCode":"function canProcessView(view) {\n  const hasQuery = view.query && Object.keys(view.query).length > 0\n  return !(hasQuery && !Array.isArray(view.query) && !view.queryUI)\n}\nif (!canProcessView(view)) throw new Error('View needs queryUI or array-form query')","typeGuard":"function hasConvertibleQuery(view) {\n  return !view.query || Array.isArray(view.query) || !!view.queryUI\n}","tryCatchPattern":"try {\n  const enriched = await sdk.views.get(viewId)\n} catch (e) {\n  if (e?.status === 400 && e?.message?.includes('queryUI')) {\n    // recreate or repair the view doc\n  } else throw e\n}","preventionTips":["Always create/update views through the SDK/API so queryUI is written alongside query","Never hand-edit view docs in CouchDB","After upgrading, audit views for object-shaped query without queryUI"],"tags":["http-400","views","data-migration","schema-validation"],"backgroundTag":"view-missing-queryui","analyzedSha":"a81a902e9a8fe55b467d106765f6638f12e35c49","analyzedAt":"2026-08-29T01:03:10.972Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}