Skip to content

refactor(spec): PoC - TypeSpec#45

Open
dschmidt wants to merge 1 commit into
opencloud-eu:mainfrom
dschmidt:refactor/typespec-poc
Open

refactor(spec): PoC - TypeSpec#45
dschmidt wants to merge 1 commit into
opencloud-eu:mainfrom
dschmidt:refactor/typespec-poc

Conversation

@dschmidt

@dschmidt dschmidt commented May 13, 2026

Copy link
Copy Markdown
Contributor

This is a PoC - please, don't shoot me 😂

Started out of curiosity: how would libre-graph-api's spec read in TypeSpec, and can the op is … / template mechanism catch the sibling-endpoint drift that easily happens with all the sibling APIs in MS Graph? api/openapi-spec/v1.0.yaml becomes a build artefact regenerated from spec/*.tsp; generated clients stay structurally unchanged.

I know this kind of PR is sensitive without prior discussion, but to me it's easier to discuss actual code than assumptions. I don't expect it to be wanted right now - after having spent a bunch of hours on it I at least want to show it here for reference at a later point.

What it buys

Duplication reduction and drift prevention between sibling endpoints. /v1.0/drives/{drive-id}/items/{item-id}/children and /v1.0/me/drive/root/children mount the same listChildren op with identical body, query and response. In hand-maintained YAML you keep those parallel by hand; here they're instantiations of the same template:

op getDriveItemChildren is DriveItemOps.listChildren<DriveAndItemIdPath>;
op getDriveRootChildren is DriveItemOps.listChildren<DriveIdPath>;
op homeGetChildren     is DriveItemOps.listChildren;

It's immediately clear these endpoints do the same thing - no parsing parameters and responses to compare them by hand. Same story for the permission ops mounted at /root/... and /items/{item-id}/..., which share a single PermissionsOps interface. Implementing (some of) #44 would add exactly this kind of duplication.

The single 6k-line YAML file is gone. Total line count stays roughly the same, but each per-domain file is a manageable size and much easier to navigate.

A few spec bugs fell out during the port: CreateInvitation's request body was wrongly optional, a handful of invitation ops were missing default: odata.error, and GET /v1.0/drives/{drive-id}/root/children exists in MS Graph but was missing here (and in the graph service). The strict typing also caught a long tail of bugs in the original examples: blocks that openapi-generator-cli had silently been swallowing (grantedToV2 as array vs single object, parentReference.driveID vs driveId, @UI.hidden vs @UI.Hidden, bare arrays vs {value: [...]} wrappers, …); each example is fixed to match the schema clients actually code against.

microsoft/typespec#10656 came out of this port as a side effect (merged, released in the pinned 1.13.0).

What it does not buy

Not a source-size win: the diff here comes out net-positive (+7.6k / -6.3k) once package-lock.json, the scripts, the Makefile and the @opExample decorators are counted in. The gain is in future change cost, not in today's diff.

Generated client surface is unchanged on the Go and TypeScript side; PHP and C++/Qt I didn't measure locally. A few @friendlyName inconsistencies inherited from the original spec stay in place - renaming would be client-breaking and is out of scope.

What it costs

Every client pipeline now runs tsp compile before the generator step, with Docker + node + the TypeSpec compiler as new dependencies. Anyone working on the spec needs to learn the template syntax and the file layout (conventions are documented in section comments).

CI / build infrastructure

api/openapi-spec/v1.0.yaml is gitignored and regenerated in CI before every client-generate step. Plain make compiles the spec, make help lists the targets for local generation. The openapi-generator-cli image pin stays in the .woodpecker/build-*.yaml files; Renovate keeps bumping it there and the Makefile greps the version out, so there's only one source of truth.

Honest pitch

It might be worth it if we aim to extend libre-graph-api with new mount points - especially ones with siblings.

@dschmidt
dschmidt marked this pull request as ready for review May 13, 2026 01:38
@dschmidt
dschmidt force-pushed the refactor/typespec-poc branch from 28cd0cd to cb8bfd6 Compare May 13, 2026 01:39
@dschmidt
dschmidt requested a review from Copilot May 13, 2026 02:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a proof-of-concept migration of the Libre Graph API specification from a hand-maintained OpenAPI YAML to TypeSpec sources, with CI and local tooling updated so api/openapi-spec/v1.0.yaml becomes a generated build artifact.

Changes:

  • Add a TypeSpec-based spec (spec/*.tsp) covering drives, permissions, users, groups, education, invitations, tags, activities, and applications.
  • Add local build tooling (Makefile + scripts) to compile the TypeSpec spec and generate clients via the existing OpenAPI Generator image pin.
  • Update Woodpecker pipelines to compile the spec before generating/publishing docs and before generating language clients; ignore generated spec output in git.

Reviewed changes

Copilot reviewed 31 out of 34 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
spec/main.tsp TypeSpec entrypoint wiring all domain files and service metadata.
spec/tspconfig.yaml Configures OpenAPI3 emission to api/openapi-spec/v1.0.yaml.
spec/common.models.tsp Adds shared cross-domain models (OData error, common query params).
spec/common.templates.tsp Adds reusable TypeSpec templates for common operations and response shapes.
spec/drives.models.tsp Ports drive + driveItem models, sharing/permission models, and shared drive query params.
spec/drives.templates.tsp Introduces DriveItem operation templates to reduce sibling endpoint drift.
spec/drives.routes.tsp Declares drive and driveItem routes, mounting the shared templates.
spec/permissions.models.tsp Adds global role definition models for permission role endpoints.
spec/permissions.templates.tsp Adds shared permission operation templates mounted at multiple parent paths.
spec/permissions.routes.tsp Declares global permission roleDefinition routes.
spec/users.tsp Ports user models and /users routes (including appRoleAssignments and photo).
spec/me.tsp Ports /me routes (user, photo, drives, home drive convenience routes).
spec/groups.tsp Ports group models and /groups routes (including members $ref endpoints).
spec/education.tsp Ports education domain models and routes under /education/*.
spec/invitations.tsp Ports invitation models and /invitations routes using shared templates.
spec/tags.tsp Ports tags extension endpoints under /extensions/org.libregraph/tags.
spec/activities.tsp Ports activities extension endpoint under /extensions/org.libregraph/activities.
spec/applications.tsp Ports applications models and /applications routes.
spec/package.json Adds Node/TypeSpec dependencies and build script for spec compilation.
spec/package-lock.json Locks TypeSpec compiler/emitter dependency graph.
spec/.gitignore Ignores node_modules/ and build/ under spec/.
scripts/compile-spec.sh Adds a script to install deps (if needed) and run tsp compile.
scripts/generate-go.sh Wraps OpenAPI Generator Go client generation in a reusable script.
scripts/generate-typescript-axios.sh Wraps OpenAPI Generator TypeScript-Axios client generation in a reusable script.
scripts/generate-php.sh Wraps OpenAPI Generator PHP client generation in a reusable script.
scripts/generate-cpp-qt-client.sh Wraps OpenAPI Generator C++/Qt client generation in a reusable script.
Makefile Adds local targets to compile spec and generate language clients using pinned generator images.
.woodpecker/build-go.yaml Adds a compile-spec step and switches generation to the shared scripts.
.woodpecker/build-typescript-axios.yaml Adds a compile-spec step and switches generation to the shared scripts.
.woodpecker/build-php.yaml Adds a compile-spec step and switches generation to the shared scripts.
.woodpecker/build-cpp-qt.yaml Adds a compile-spec step and switches generation to the shared scripts.
.woodpecker/docs.yaml Adds a compile-spec step before docs deployment.
.gitignore Ignores generated OpenAPI output and local build artifacts.
Files not reviewed (1)
  • spec/package-lock.json: Language not supported
Comments suppressed due to low confidence (1)

.woodpecker/docs.yaml:33

  • The new compile-spec step will create spec/node_modules, and the subsequent rsync -a ... . deploys the entire workspace. This will upload node_modules (and other local build artefacts) to the docs host, increasing deploy size/time and potentially exhausting storage. Add rsync excludes for spec/node_modules, spec/build, build/, etc., or delete those directories before rsync.
  - name: compile-spec
    image: *node_image
    commands:
      - sh scripts/compile-spec.sh
  - name: deploy-staging
    image: debian:latest
    environment:
      SSH_KEY:
        from_secret: ssh-key-staging
      REMOTE_HOST:
        from_secret: remote-host-staging
    when:
      event: push
      branch: ${CI_REPO_DEFAULT_BRANCH}
    commands:
      - apt-get update
      - apt-get install -y openssh-client rsync
      - mkdir -p $HOME/.ssh
      - ssh-keyscan -t rsa $REMOTE_HOST >> $HOME/.ssh/known_hosts
      - echo "$SSH_KEY" > "$HOME/.ssh/id_rsa"
      - chmod 0600 $HOME/.ssh/id_rsa
      - rsync -a --exclude '.git' --delete . $REMOTE_HOST:/var/swagger/libre-graph-api
  - name: deploy-production

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread scripts/compile-spec.sh Outdated
Comment thread .woodpecker/docs.yaml
Comment thread spec/drives.models.tsp Outdated
Comment thread spec/education.tsp Outdated
Comment thread spec/drives.routes.tsp Outdated
dschmidt added a commit to dschmidt/libre-graph-api that referenced this pull request May 13, 2026
- `.woodpecker/docs.yaml`: rsync now excludes `spec/node_modules`,
  `spec/build`, `build/` so the deploy host doesn't receive the
  TypeSpec compiler's install tree (which the new `compile-spec` step
  creates).
- `scripts/compile-spec.sh`: reinstall when the lockfile is newer than
  `node_modules/.package-lock.json`, not only when `node_modules` is
  missing. Renovate-driven dependency bumps would otherwise be silently
  ignored by stale local caches.
- Three docstring typos (`avalable` -> `available`, `And extension` ->
  `An extension`, `childrens` -> `children`).
@dschmidt
dschmidt requested a review from Copilot May 13, 2026 03:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 31 out of 34 changed files in this pull request and generated 6 comments.

Files not reviewed (1)
  • spec/package-lock.json: Language not supported
Comments suppressed due to low confidence (1)

scripts/compile-spec.sh:14

  • The install-marker check uses node_modules/.package-lock.json, but the script never creates/updates that file after npm ci. As written, npm ci will rerun every time compile-spec.sh is invoked (because -nt is true when the marker file is missing). Consider creating/updating the marker after a successful install (e.g., copy the lockfile into place) or drop the marker logic and always run npm ci in CI while keeping a separate local-cache strategy.
npm ci --prefer-offline --no-audit --no-fund

npx tsp compile .

Comment thread Makefile
Comment thread spec/users.tsp
Comment thread spec/permissions.routes.tsp
Comment thread spec/tags.tsp
Comment thread spec/tags.tsp Outdated
Comment thread spec/permissions.models.tsp Outdated
dschmidt added a commit to dschmidt/libre-graph-api that referenced this pull request May 13, 2026
- `Makefile`: pass `HOME=/tmp` + `npm_config_cache=/tmp/.npm` into the
  spec compile container so `npm ci` can write its cache/state when
  the host uid/gid doesn't match `/home/node` in `node:22-alpine`.
- `spec/common.models.tsp`: move `SearchParam` here from `groups.tsp`.
  It's a generic OData `$search` query parameter and was creating an
  unnecessary `users.tsp -> groups.tsp` dependency.
- `spec/permissions.routes.tsp`: fix the role-id path parameter in each
  `GetPermissionRoleDefinition` `@opExample` so it matches the role id
  in the corresponding response body. Also align the File Drop
  `allowedResourceActions` value (`upload/create`) with the list
  endpoint's example.
- `spec/tags.tsp`: make the request body required on `assignTags` and
  `unassignTags` (the body schemas require `resourceId` and `tags`, so
  a body-less call can't succeed). Update `TagsOk`'s doc from
  "No content" to "Success" since the status code is 200 with empty
  body.
- `spec/permissions.models.tsp`: typo `than can be used` ->
  `that can be used`.
dschmidt added a commit to dschmidt/libre-graph-api that referenced this pull request Jul 2, 2026
- `.woodpecker/docs.yaml`: rsync now excludes `spec/node_modules`,
  `spec/build`, `build/` so the deploy host doesn't receive the
  TypeSpec compiler's install tree (which the new `compile-spec` step
  creates).
- `scripts/compile-spec.sh`: reinstall when the lockfile is newer than
  `node_modules/.package-lock.json`, not only when `node_modules` is
  missing. Renovate-driven dependency bumps would otherwise be silently
  ignored by stale local caches.
- Three docstring typos (`avalable` -> `available`, `And extension` ->
  `An extension`, `childrens` -> `children`).
dschmidt added a commit to dschmidt/libre-graph-api that referenced this pull request Jul 2, 2026
- `Makefile`: pass `HOME=/tmp` + `npm_config_cache=/tmp/.npm` into the
  spec compile container so `npm ci` can write its cache/state when
  the host uid/gid doesn't match `/home/node` in `node:22-alpine`.
- `spec/common.models.tsp`: move `SearchParam` here from `groups.tsp`.
  It's a generic OData `$search` query parameter and was creating an
  unnecessary `users.tsp -> groups.tsp` dependency.
- `spec/permissions.routes.tsp`: fix the role-id path parameter in each
  `GetPermissionRoleDefinition` `@opExample` so it matches the role id
  in the corresponding response body. Also align the File Drop
  `allowedResourceActions` value (`upload/create`) with the list
  endpoint's example.
- `spec/tags.tsp`: make the request body required on `assignTags` and
  `unassignTags` (the body schemas require `resourceId` and `tags`, so
  a body-less call can't succeed). Update `TagsOk`'s doc from
  "No content" to "Success" since the status code is 200 with empty
  body.
- `spec/permissions.models.tsp`: typo `than can be used` ->
  `that can be used`.
@dschmidt
dschmidt force-pushed the refactor/typespec-poc branch from 81a09f9 to d0b1f9b Compare July 2, 2026 08:44
api/openapi-spec/v1.0.yaml becomes a build artefact compiled from
spec/*.tsp; every client pipeline runs the compile step before
generating, and a Makefile + scripts cover local generation. Sibling
endpoints share templated operations (DriveItemOps, PermissionsOps,
LibreGraphOps) so identical routes cannot drift apart. Pins TypeSpec
1.13.0.

Semantic changes against the previous YAML:
- add GET /v1.0/drives/{drive-id}/root/children and
  GET /v1.0/drives/{drive-id}/items/{item-id}/children
- CreateInvitation's request body is now required
- invitation ops gained the missing 'default: odata.error' response
- example blocks fixed to match their schemas (grantedToV2 as single
  object, parentReference.driveId casing, @UI.Hidden,
  onPremisesSamAccountName, {value: [...]} collection wrappers)

Generated Go and TypeScript clients are structurally unchanged.
@dschmidt
dschmidt force-pushed the refactor/typespec-poc branch from d0b1f9b to 2827f60 Compare July 2, 2026 08:47
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.

2 participants