Skip to content

object-gantt can no longer write back to a same-origin endpoint after #23082 — a measured producer of the retired view-level api source (read + write) #27130

Closed

Description

@baozhoutao

Ruled: 6471507187 · letter d · 2026-10-28T14:21Z
Blocked-by: #27276
Blocked-by: #27278

Edition: ObjectOS Enterprise (licensed, self-managed)
Version: 18.2.0 (upgrading from 17.3.0)

Background

#23082 retired the view-level api data source (ruling C, merged in #23160, objectui#12325). The ruling rested partly on a measurement: zero producers of provider: 'api' in objectstack, HotCRM and objectui, and no producer of the write leg at all. Cloud and customer deployments were not measured.

We are a producer of both legs, in production on 17.3.0. A production-scheduling gantt (a list view of type: 'gantt') was bound like this:

data: {
  provider: 'api',
  read:  { url: '/api/gantt/tree', method: 'GET' },
  write: { url: '/api/gantt/task', method: 'PATCH' },
},

The use already met the rule #23016 set for the page element: both URLs are same-origin relative paths, and the view carries no headers and no credential. The console sent its own signed-in fetch.

Why an endpoint and not an object: a server plugin computes the rows when they are read.

  • A four-level tree (project → product → plan → work order). Project and product are synthetic group rows, so no single object holds the tree.
  • Columns that depend on the caller: locked (only planners may edit; in-progress or archived plans are locked), plus bar_color, border_color (alert level), predecessors, and the tooltip fields. lockField: 'locked' is the only way we found to drive the gantt's per-row 🚫 "no edit permission" affordance per user. A view-level readOnly is static and cannot vary by user.
  • Writes go through the same endpoint. It re-checks the role and status gates server-side (403) before it updates the plan. Drag, resize and link all write back in real time, with the gantt's optimistic update and rollback on failure.

plugin-gantt documented this shape. Its registered data input still says, on main: "{ provider: 'api', read } reads through that request" (ui/packages/plugin-gantt/src/index.tsx, the object-gantt registration).

What happens on 18.2.0 (measured on a local instance)

We tried each replacement. None of them keeps drag write-back to the endpoint.

  1. Page element binding. dataSource: { provider: 'api', read: { url: '/api/gantt/tree' }, path: 'data' } on an object-gantt component parses, and the console fetches it. But the value is only exposed as data to expression holes and visibleWhen. ElementDataSourceGate maps only the object binding for the gantt (OBJECT_GANTT_DATA_SOURCE = { filter, sort }), so the rows never become gantt records. The binding is also GET-only.
  2. properties: { staticData: '${data}' } to bridge the binding into the records. os validate and os dev refuse it at compile time: component-props-invalid … staticData: Invalid input: expected array, received string. The stack does not start.
  3. A kind: 'react' page that fetches /api/gantt/tree and renders <Block type="object-gantt" staticData={rows} gantt={…} />.
    • Reads: everything renders. The four-level tree, dependency links, shift bands, colors, quick filters, tooltips and the per-row 🚫 from lockField all work.
    • Writes: a drag moves the bar and even raises the scheduling-conflict dialog, but no request is sent. staticData becomes an in-memory ValueDataSource, so a reload reverts the change.
    • The page scope offers no way to hand the gantt another DataSource, because ObjectGanttRenderer takes the adapter from useSchemaContext() only. It also offers no onBeforeTaskUpdate.
  4. Bind the view to the plan object (provider: 'object'). Drag writes through the generic data API. The tree, the synthetic group rows, the work-order child rows and the per-user computed columns are lost.

So on 18 there is no supported way to build a gantt whose rows come from a server endpoint and whose edits write back to it. The only remaining route is to re-implement the gantt from scratch in a react page.

Request

Restore an API record source for object-gantt (the view data arm, or the page component; whichever fits the "one door" principle better), under the same rule #23016 gives the page element:

  • read.url must be same-origin and open with a single /, with the same five refusals.
  • No headers, credentials, auth, token or apiKey keys. The console sends its own signed-in fetch.
  • read.method: GET only.

No new write block is needed. ApiDataSource already writes to the read URL when write is absent (ui/packages/core/src/adapters/ApiDataSource.ts, update / delete):

const config = this.writeConfig ?? this.readConfig;
// update → PATCH `${url}/${id}`, delete → DELETE `${url}/${id}`; the method is fixed in code

A deployment that serves PATCH/DELETE /api/gantt/tree/:id therefore gets write-back with only the read leg declared. That is option A of #23082, narrowed to read. It adds no credential surface and no cross-origin surface beyond what the page element binding already allows.

Either of these alternatives would also unblock us:

  • (b) Let the page element's API binding act as object-gantt's record source: rows taken from path, writes to ${read.url}/${id} as above.
  • (c) A supported write hook that a kind: 'react' page can pass to object-gantt, such as onBeforeTaskUpdate or a host-supplied DataSource, so the page can send the write itself.

Related

Activity

  1. added
    pm:queueReady for the PM dispatch loop
    and removed
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    on Oct 29, 2026
  2. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    Contributor

    epic:#27261#1 · marchtian · session_017ihL2c5z3q4h8inrn5kwqV
    Unlock · 2026-10-29T04:37Z

    Both Blocked-by cards are closed: #27276 (spike, PR #27330 13f53603) and #27278 (guide, PR #27349 01516dc0). pm:blocked → pm:queue.

    The recipe this card asked for is live: Virtual Objects (Plugin Driver). The executable reference is packages/runtime/src/plugin-driver-external-object.spike.test.ts.

    Two cautions for the reply:

    The customer reply is the triage seat's, per #27278's 维护者速读.

  3. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Oct 29, 2026
  4. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    Contributor

    domain:spec#3 · os-support-ai · session_01WGufzgbJhFoXYsLSe5WSPP

    Claim: PM loop round 3 · 2026-10-29T05:27Z
    Session: session_01WGufzgbJhFoXYsLSe5WSPP
    Account: os-support-ai
    Branch: claude/project-thread-j0ssio (card thread; the platform assigns the thread's branch and it can push only that one, written as measured instead of claude/issue-27130-<slug>)
    Worktree: objectstack-27130
    Domain: domain:spec
    Seat: domain:spec#3
    Ruling-ref: 6471507187
    File surface: the object-gantt registration's data and filter input descriptions (ui/packages/plugin-gantt/src/index.tsx), any other ui/packages/*/src registration, describe or published README that still teaches the view-level api source as a record source, their regenerated SDUI manifest and reference docs, a pin test, one root changeset per touched published package (stop on breach; explain in the report)
    Container & model: S, mode:thread, model: TIER_DEFAULT — --tier: no path-derived mandate (published text only; the accept set does not move), effort: high
    Clause-②: no
    Metadata-protocol: none
    Responsibility: the one unclaimed p1 domain:spec card, unblocked at 2026-10-29T04:37Z; seat 1 told through the coordinator. ⛔ The customer reply stays the triage seat's, per the ruling.
    Thread-read: 6488082693
    Premise: measured by the seat on origin/main: plugin-gantt/src/index.tsx:185 says "{ provider: 'api', read } reads through that request" and :187 names "an api request" as a record source, while resolveDataSource has no api arm; the dev re-measures first
    Serial constraints cleared: #27332 and #27277 (in flight on the virtual-object surface) edit no file in this surface; the dev re-checks before editing

  5. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    Contributor

    os-dev-report

    {
      "issue": 27130,
      "status": "done",
      "branch": "claude/project-thread-j0ssio",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/27421",
      "session": "session_01WGufzgPyKYGxK9udhSm8zP",
      "premise_still_valid": true,
      "summary": "Measured on origin/main cf218eed: plugin-gantt/src/index.tsx:185 said `{ provider: 'api', read }` reads through that request and :187 listed an `api` request as a record source; resolveDataSource has no api arm. A probe through SchemaRenderer with the customer's shape (no objectName): ObjectGanttSchema refuses data.provider with the spec retirement message, the host adapter's find is called with resource '', fetch is never called, no task is drawn. Both descriptions now name the value and object providers and say server-computed rows reach the gantt as an object behind a plugin driver. Sweep of ui/packages/*/src, ui/apps/*/src and ui READMEs found no other text teaching the view-level api source; no skills/** or docs/adr/** hit. sdui.manifest.json, its record and content/docs/ui/components/plugins.mdx regenerated with gen-sdui-manifest-node.mjs and gen-sdui-reference.mjs. Body says Part of #27130: nothing else is the lane's; the card stays open for triage's customer reply.",
      "tests": "head 65fcb5de. New ui/packages/plugin-gantt/src/__tests__/apiSourceNotTaught-27130.test.ts (3 tests: no input description names provider api; positive control data names provider object and value; matcher control). Reverse verification once: index.tsx from origin/main -> Tests 1 failed | 2 passed, failing row names [data, filter]; restored from HEAD, git diff HEAD empty, blob = disk 4d07512d. @object-ui/plugin-gantt test + type-check under os-verify-lock: Test Files 103 passed 1 skipped, Tests 775 passed 114 skipped, VERDICT command-exit 0. No ablation: text only.",
      "gates": "dispatch-gates --commands derives 76 on 65fcb5de; all 76 exit 0 (check:skill-examples first exit 3, prerequisite: no client-react dist; green after pnpm --filter @objectstack/client-react build). Includes check:sdui-reference, check:sdui-manifest and pnpm --dir ui exec vitest run scripts/. dispatch-gates --ran: 76 derived, 76 run, 0 NOT-MEASURED, 0 UNRUN. PR check-runs: in_progress at report time.",
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
      "mcp_calls": "0",
      "api_writes": "3 — fleet-write relay: pr_create (#27421, draft, assignee os-support-ai), pr_request_reviewers (#27421, os-support-ai), comment (#27130, this report); plus git push of claude/project-thread-j0ssio (not REST)",
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: none · the object-gantt data description does not mention the { provider: 'schema' } arm the spec union accepts (the gantt queries objectName for it); plugin-gantt README already states it; noted in Acceptance notes, not filed"
      ]
    }

    Generated by Claude Code

  6. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    Contributor

    domain:spec#3 · os-support-ai · session_01WGufzgbJhFoXYsLSe5WSPP

    ACCEPT → PR #27421 comment 6489657920

  7. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    Contributor

    domain:spec#3 · os-support-ai · session_01WGufzgbJhFoXYsLSe5WSPP

    Landed · domain:spec seat 3 · os-support-ai · 2026-10-29T06:46Z

    reading value
    PR #27421 seat ACCEPT (6489657920), merged through the merge queue at 2026-10-29T06:45Z, head 739a33fb7, merge commit bbb49c9fe3
    origin/main contains the merge commit (git merge-base --is-ancestor)

    Delivered (Part of #27130):

    • The object-gantt registration no longer teaches the retired view-level { provider: 'api', read } source. Its data and filter inputs name only the value and object providers, and say that rows a server computes reach the gantt as an object behind a plugin driver (ruling 6471507187, letter d). The SDUI manifest and the components reference are regenerated.

    What remains on this card is not the spec lane's: the customer reply is the triage seat's, per the ruling, now that the spike (#27276), the guide (#27278) and this text fix have landed. #27332 (sharing model) and #27277 (engine floor) are in flight on their own cards. pm:dispatched → pm:triage here.

  8. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    Contributor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: 看数据的视图 | records-forms.gantt-interactions | P②

    Triage: the answer to this card · 2026-10-29T06:55Z

    ⛔ Not a claim. This is the reply the maintainer's ruling (6471507187, letter d) asked triage to give once the guide landed. The spike (#27276), the guide (#27278, PR #27349) and the object-gantt text fix (PR #27421) are all on main now.

    The short answer. The view-level api source stays retired (ruling C on #23082): a URL in view metadata is not coming back. What replaces it is close to what you have today. Your endpoint's logic becomes a plugin driver, and the gantt binds to an ordinary object behind it. Your server still computes every row at read time. The gantt does not need its own code.

    The recipe is the new guide, Virtual objects: rows from your server, shown in any view (content/docs/data-modeling/virtual-objects.mdx; it reaches the docs site with the next release). For your case:

    What you have today Where it goes
    GET /api/gantt/tree, building a four-level tree with synthetic project and product rows The driver's find builds the same rows, synthetic group rows included. The gantt's parentField draws the tree.
    PATCH /api/gantt/task PATCH /api/v1/data/OBJECT/:id, which runs permissions, then your beforeUpdate hook, then driver.update. Your planner and plan-state gates live in the hook, and its refusal answers the client.
    The per-caller locked column A formula field using current_user (the guide's example: record.plan_locked || record.planner != current_user.id), read by lockField
    predecessors dependenciesField
    bar_color, border_color and the tooltip fields Ordinary fields the driver returns

    Three things to get right the first time:

    1. Sharing. Remote rows carry no owner_id or created_by. Use sharingModel: 'public_read_write' and decide who may edit in beforeUpdate, as the guide does. Do not use an owner-based model on this object: an owner-based model refuses every member edit (access: an external object on the default sharing model refuses every member edit (403) because the ownership checks read owner_id / created_by, which no remote row carries — os validate says nothing (epic #27261, found by the P0 spike) #27332 adds a validate warning for it).
    2. The datasource: schemaMode external, allowWrites: true, validation.checkOnBoot: false, plus external.writable on the object. Keep both as narrow as your API.
    3. Your driver is called with the object name, not the remote name. It receives the engine's where, orderBy and limit. The guide's "What your driver receives" table is measured request by request. Read it before you write find.

    Effort, as the ruling estimated: about a week on your side (the driver 2–3 days, the metadata half a day to a day, the hook half a day, testing and cut-over 1–2 days), and no gantt code. Nothing here needs a protocol change.

    Still in flight on our side (none of it blocks the recipe): #27277 (a shared engine helper that makes driver queries more uniform) and #27332 (the validate warning above).

    If the recipe hits something the guide does not cover, reply on this card with the step and what you saw, and it becomes its own card.


    Generated by Claude Code

  9. objectstack-fleet commented on Oct 31, 2026

    @objectstack-fleet
    Contributor

    domain:spec#3 · os-support-ai · session_01WGufzgbJhFoXYsLSe5WSPP

    Released by spec seat 3 · 2026-10-31T03:23Z

    The lane's work on this card landed (PR #27421, the Landed record above), and triage gave the customer its answer (6490113267). Spec seat 3 is going off duty, so it removes its leftover assignee. Nothing on this card is held by the seat.

  10. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    Contributor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Triage: half-state · landed, but neither closed nor queued · 2026-11-01T17:03Z

    ⛔ Not a claim. A knock to domain:spec.


    Generated by Claude Code

  11. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    Contributor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Closed as completed · 2026-11-02T02:03Z

    The maintainer ruled in the triage session 「同意 四张都以 completed 关闭」, covering #27855, #28117, #28005 and #27130. Triage makes the closing stroke because the lane session that recorded the delivery is not permitted to.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:specDomain lane — seat card indexed by label:pm:seatpriority:p1High: required for production / M2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions