Skip to content

refactor: migrate Graph calls to libre-graph-api-go SDK (#13) - #24

Open
dj4oC wants to merge 3 commits into
mainfrom
refactor/libregraph-sdk-migration
Open

refactor: migrate Graph calls to libre-graph-api-go SDK (#13)#24
dj4oC wants to merge 3 commits into
mainfrom
refactor/libregraph-sdk-migration

Conversation

@dj4oC

@dj4oC dj4oC commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Problem

Per #13, the server hand-rolls LibreGraph paths (/graph/v1beta1/...) and response shapes, which drift from the API — the source of bugs like #11 (wrong version prefix) and the share-invite envelope bug below.

What changed

  • Adds libre-graph-api-go and an auth-aware SDK clientclient.GraphClient(). A graphTransport wraps the existing HTTP transport and re-applies the configured auth (app-token Basic / OIDC Bearer) and the 429-retry, so version prefixes and request/response shapes are maintained by the SDK.
  • shares.go, spaces.go, workflows.go: every SDK-coverable Graph call now uses DrivesPermissionsApi / DrivesApi / DrivesRootApi / MeDriveApi.
  • SDK response models are mapped onto the existing tool output structs via sdkConvert (a JSON round-trip), so each tool's JSON output contract is unchanged.

Bug fixed along the way

The /invite response envelope is {"value":[...]} (verified against oCIS 8.0.1), not the {"permissions":[...]} the hand-rolled InviteOutput parsed — so create_share and invite_to_space previously returned an empty permission list even on a successful 200. The SDK's CollectionOfPermissions{Value} fixes this.

Kept hand-rolled (scoped — not covered by the SDK)

Area Why
WebDAV (upload/propfind/search/download, empty_trashbin, set_space_image/readme) SDK is Graph-only
get_sharing_roles + sharing-roles resource SDK roleDefinitions return type is code-gen-broken (singular *UnifiedRoleDefinition vs the live JSON array)
accept_share no SDK method for the accept action
list_spaces / list_my_spaces SDK list builders expose only $filter/$orderby — no $top/$skip, so migrating would drop the tool's pagination
delete_space (purge) needs the Purge: T request header
restore_space needs @UI.Hidden, absent from DriveUpdate

The internal GetJSON/ListJSON/PostJSON/… helpers remain only for these scoped endpoints (and the not-yet-migrated tool files).

⚠️ Dependency note

The drivesPermissions / activities / driveItem APIs are not in any tagged SDK release (latest v1.0.4 lacks them); they exist only on the SDK's main. This PR therefore pins libre-graph-api-go at a main pseudo-version (v1.0.5-0.20260528...). Recommend cutting a libre-graph-api-go release and switching to the tag. (Context on #13.)

Testing

  • go build ./..., go test -race ./..., go vet ./..., golangci-lint run ./... all pass; coverage 74.4% (> 70%).
  • Verified end-to-end against a live oCIS 8.0.1: share create/list/createLink/update/delete; space create/get/invite/list-permissions/update/disable; workflow create-project-space (with members) + get-space-overview — including that the {"value"} envelope fix now returns permissions.

Refs #13

@kw-security

kw-security commented Jun 14, 2026

Copy link
Copy Markdown

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues
Code Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@dj4oC

dj4oC commented Jun 14, 2026

Copy link
Copy Markdown
Contributor Author

Filed the two upstream libre-graph-api-go issues this PR depends on / works around:

@DeepDiver1975 DeepDiver1975 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: solid, behavior-preserving refactor with a real bug fix. Reviewed as a maintainer; verified against libre-graph-api-go source and the repo. Not approving/merging (review-only), but no blocking correctness issues found.

What I verified:

  • SDK paths match the hand-rolled ones exactly. Server URL is baseURL + "/graph" (graph.go:59) and the SDK templates resolve to the same routes the old code used: DrivesPermissionsApi.Invite -> /v1beta1/drives/{d}/items/{i}/invite, DrivesRootApi.InviteSpaceRoot -> /v1beta1/drives/{d}/root/invite, MeDriveApi.ListSharedByMe/WithMe -> /v1beta1/me/drive/sharedByMe|sharedWithMe, DrivesApi.Get/Create/Update/DeleteDrive -> /v1.0/drives. Version prefixes are preserved, as claimed.
  • Auth + 429 retry preserved: graphTransport.RoundTrip re-applies applyAuth and retries on 429 (graph.go:74-102). Body replay via GetBody clone is correct; bodyless requests fall through unchanged.
  • Invite envelope fix is real and correct. NewDriveRecipient() in the SDK defaults @libre.graph.recipient.type to "user" (model_drive_recipient.go), and the response is CollectionOfPermissions{Value} — so mapping resp.GetValue() onto InviteOutput{Permissions} fixes the previously-empty list. The updated test asserting output.Permissions[0].ID == "p1" covers it.
  • The "kept hand-rolled" rationale checks out: DriveUpdate has no @UI.Hidden field, confirming restore_space can't migrate.

Notes (non-blocking):

  • sdkConvert relies on SDK models and local structs sharing json tags. That holds for the fields these tools expose, but it's an implicit contract - a future SDK json-tag rename would silently drop a field rather than fail to compile. A short comment or a targeted test on one mapped struct (e.g. Drive) would harden it.
  • The main pseudo-version pin is the right call to flag. Agree with cutting a tagged libre-graph-api-go release and switching before this is depended on long-term.
  • Overlaps with the invite recipient-type fix in PR 26 (handleInviteToSpace/handleUploadAndShare). This PR already fixes those two paths via the SDK default, so whichever merges second will need a rebase; PR 26's other three fixes are independent and still needed.

CI: Build & Test, Lint, and all three Snyk checks green.

🤖 Generated with Claude Code

@2403905

2403905 commented Jul 6, 2026

Copy link
Copy Markdown

PR #24 Review — refactor: migrate Graph calls to libre-graph-api-go SDK

Overall verdict: Solid, behavior-preserving refactor with a genuine bug fix. The structural approach is sound. A few issues worth addressing before merge.


Bug fix (verified correct)

The {"value":[...]} envelope fix is real and confirmed by the updated tests. The old InviteOutput{Permissions:[...]} envelope assumption meant create_share and invite_to_space always returned empty permission lists on success. sdkConvert[]Permission → InviteOutput{Permissions: perms} fixes it
correctly.


Issues

  1. GraphClient() allocates a new *http.Client + *libregraph.APIClient on every tool call (internal/client/graph.go:52)

Every tool handler calls c.GraphClient() which constructs a new &http.Client{...} and libregraph.NewAPIClient(cfg) per invocation. HTTP clients are designed to be shared — they carry connection pools. Creating one per call defeats connection reuse and leaks goroutines (each http.Client has its own idle-connection
manager). The fix is to cache the *libregraph.APIClient on the Client struct (initialized in New() or lazily with sync.Once).

  1. sdkConvert is a silent contract between JSON tags (internal/tools/sdk.go:124)

The round-trip works today because SDK models and local structs share json tags for the fields the tools expose. But a future SDK json-tag rename silently drops the field rather than failing to compile. The existing test coverage partially mitigates this (e.g. output.Permissions[0].ID is asserted), but a Drive
struct mapping test would close the gap for the most complex model.

  1. createProjectSpace silently discards sdkConvert failure (internal/tools/workflows.go:726-729)

default:
if perms, cerr := sdkConvert[]Permission; cerr == nil {
out.Permissions = perms // error branch: Permissions stays nil
}
out.Message = "space created with members" // success message even if conversion failed

If sdkConvert fails (a programming error, not a server error), permissions are silently omitted but the message says "space created with members". Pre-existing pattern from the old code, but now there's a new failure mode. At minimum, log or include the conversion error in the message.

  1. Pseudo-version pin (go.mod:12)

Pinning libre-graph-api-go at a main pseudo-version is clearly the right pragmatic call (tagged releases lack the APIs needed). The PR acknowledges this. The upstream issues (#10, #11) are filed. This should block a stable v2.0 release but is fine for merging now.


Minor

  • The switch { case err != nil: ... default: ... } pattern in workflows.go:722 is equivalent to if err != nil { ... } else { ... } — slightly unconventional Go.
  • graphTransport.RoundTrip calls applyAuth(req) once before the loop (correct), but the comment says "re-applies" — it's applied exactly once, not per attempt. The auth header persists on clones because req.Clone copies headers. Not a bug, just a slightly misleading comment.
  • handleDeleteShare and handleRejectShare both call DrivesPermissionsApi.DeletePermission — same behavior as before. If reject_share is supposed to be a distinct action (e.g. decline a received share vs. remove a permission you granted), this is worth a follow-up issue.

dj4oC and others added 3 commits July 13, 2026 14:00
Part of #13. Adds the SDK dependency and an auth-aware SDK client
(client.GraphClient(): a graphTransport reuses the existing http client's
transport and re-applies auth + 429 retry, so the version prefixes and request
shapes are maintained by the SDK instead of hand-rolled paths).

shares.go: create/createLink/list/update/updateExpiration/delete/reject and
sharedByMe/sharedWithMe now use DrivesPermissionsApi / MeDriveApi. SDK responses
are mapped onto the existing tool output structs via sdkConvert (JSON
round-trip) so the tool JSON contract is unchanged.

This also fixes a latent bug: the /invite response envelope is {"value":[...]}
(verified against oCIS 8.0.1), not the {"permissions":[...]} the hand-rolled
InviteOutput parsed — so create_share previously returned an empty permission
list even on success.

Kept hand-rolled (not covered by the SDK): accept_share (no SDK method) and
get_sharing_roles (SDK roleDefinitions return type is code-gen-broken — singular
vs the live JSON array).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: David Walter <david.walter@kiteworks.com>
Part of #13. get/create/update/disable space now use DrivesApi; invite/
createLink/listPermissions on the space root use DrivesRootApi. SDK responses
map onto the existing Drive/Permission output structs via sdkConvert.

invite_to_space also gains the recipient-type fix (NewDriveRecipient defaults
@libre.graph.recipient.type) and the {"value":[...]} envelope fix, matching
create_share.

Kept hand-rolled (not cleanly SDK-expressible): list_spaces / list_my_spaces
(SDK list builders expose only $filter/$orderby, no $top/$skip — migrating would
drop the tool's pagination), delete_space (needs the Purge:T header), restore_space
(needs @UI.Hidden, absent from DriveUpdate), and the WebDAV space tools
(empty_trashbin, set_space_image, set_space_readme).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: David Walter <david.walter@kiteworks.com>
…go SDK

Part of #13. The Graph-backed steps now use the SDK:
- upload_and_share: invite via DrivesPermissionsApi (+ recipient-type and
  {"value":[...]} envelope fix)
- create_project_space: CreateDrive + InviteSpaceRoot
- share_with_link: CreateLink
- get_space_overview: GetDrive + ListPermissionsSpaceRoot

The WebDAV steps (upload, propfind, search, download) stay hand-rolled — the
SDK is Graph-only. find_and_download remains entirely WebDAV.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: David Walter <david.walter@kiteworks.com>
@dj4oC
dj4oC force-pushed the refactor/libregraph-sdk-migration branch from 9dd4086 to 07ba4ac Compare July 13, 2026 12:02
@dj4oC
dj4oC requested a review from a team as a code owner July 13, 2026 12:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants