From 2b569c447f07f77c5eacbfd4cf213e46d47f625c Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Tue, 12 May 2026 21:59:41 +0200 Subject: [PATCH 01/45] feat(spec): scaffold TypeSpec sources for me namespace Adds a TypeSpec project under spec/ that compiles to OpenAPI 3.0 via @typespec/openapi3. Covers the 8 me-namespace operations: - GET/PATCH /v1.0/me - POST /v1.0/me/changePassword - GET/PUT/PATCH/DELETE /v1.0/me/photo/$value - GET /v1.0/me/drives operationId, tag, and x-ms-* extensions are pinned per operation to match the existing v1.0.yaml. Schemas referenced transitively but defined in other domains (driveItem, quota, group, appRoleAssignment) are stubbed as empty models for now; they will be filled in when their domains are migrated. --- spec/.gitignore | 2 + spec/main.tsp | 21 + spec/me.tsp | 187 +++++++ spec/models.tsp | 244 +++++++++ spec/package-lock.json | 1171 ++++++++++++++++++++++++++++++++++++++++ spec/package.json | 15 + spec/tspconfig.yaml | 8 + 7 files changed, 1648 insertions(+) create mode 100644 spec/.gitignore create mode 100644 spec/main.tsp create mode 100644 spec/me.tsp create mode 100644 spec/models.tsp create mode 100644 spec/package-lock.json create mode 100644 spec/package.json create mode 100644 spec/tspconfig.yaml diff --git a/spec/.gitignore b/spec/.gitignore new file mode 100644 index 0000000..b38db2f --- /dev/null +++ b/spec/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +build/ diff --git a/spec/main.tsp b/spec/main.tsp new file mode 100644 index 0000000..ab3b3b9 --- /dev/null +++ b/spec/main.tsp @@ -0,0 +1,21 @@ +import "@typespec/http"; +import "@typespec/openapi"; +import "@typespec/openapi3"; + +import "./models.tsp"; +import "./me.tsp"; + +using Http; +using OpenAPI; + +@service(#{ title: "Libre Graph API" }) +@info(#{ + version: "v1.0.8", + license: #{ + name: "Apache 2.0", + url: "https://www.apache.org/licenses/LICENSE-2.0.html", + }, +}) +@doc("Libre Graph is a free API for cloud collaboration inspired by the MS Graph API.") +@server("https://localhost:9200/graph", "OpenCloud Development Setup") +namespace LibreGraph; diff --git a/spec/me.tsp b/spec/me.tsp new file mode 100644 index 0000000..923a139 --- /dev/null +++ b/spec/me.tsp @@ -0,0 +1,187 @@ +import "@typespec/http"; +import "@typespec/openapi"; + +import "./models.tsp"; + +using Http; +using OpenAPI; + +namespace LibreGraph; + +// ----------------------------------------------------------------------------- +// Response helpers +// ----------------------------------------------------------------------------- + +@doc("Retrieved entity") +model UserRetrieved { + @statusCode _: 200; + @body body: user; +} + +@doc("Success") +model UserSuccess { + @statusCode _: 200; + @body body: user; +} + +@doc("Success") +model NoContentSuccess { + @statusCode _: 204; +} + +@doc("Retrieved profile photo") +model PhotoRetrieved { + @statusCode _: 200; + @header contentType: "image/jpeg"; + @body body: bytes; +} + +@doc("Retrieved spaces") +model DrivesRetrieved { + @statusCode _: 200; + @body body: drivesResponse; +} + +model PhotoBody { + @header contentType: "image/jpeg"; + + @doc("New user photo") + @body + body: bytes; +} + +// ----------------------------------------------------------------------------- +// Shared query parameters (rendered as `#/components/parameters/`) +// ----------------------------------------------------------------------------- + +@friendlyName("orderby") +model OrderBy { + @query + @doc("The $orderby system query option allows clients to request resources in either ascending order using asc or descending order using desc.") + `$orderby`?: string; +} + +@friendlyName("drivesFilter") +model DrivesFilter { + @query + @doc("Filter items by property values") + `$filter`?: string; +} + +// ----------------------------------------------------------------------------- +// /v1.0/me — current-user operations +// ----------------------------------------------------------------------------- + +@route("/v1.0/me") +namespace Me { + + @summary("Update the current user") + @operationId("UpdateOwnUser") + @tag("me.user") + @patch + op updateOwnUser( + @doc("New user values") + @body + body: userUpdate, + ): UserSuccess | OdataError; + + @summary("Get current user") + @operationId("GetOwnUser") + @tag("me.user") + @extension("x-ms-docs-operation-type", "operation") + @get + op getOwnUser( + @doc("Expand related entities") + @query(#{ explode: false }) + `$expand`?: "memberOf"[], + ): UserRetrieved | OdataError; +} + +// ----------------------------------------------------------------------------- +// /v1.0/me/changePassword +// ----------------------------------------------------------------------------- + +model passwordChange { + currentPassword: string; + newPassword: string; +} + +@route("/v1.0/me/changePassword") +namespace MeChangePassword { + + @summary("Change your own password") + @operationId("ChangeOwnPassword") + @tag("me.changepassword") + @extension("x-ms-docs-operation-type", "operation") + @post + op changeOwnPassword( + @doc("Password change request") + @body + body: passwordChange, + ): NoContentSuccess | OdataError; +} + +// ----------------------------------------------------------------------------- +// /v1.0/me/photo/$value +// ----------------------------------------------------------------------------- + +@route("/v1.0/me/photo/$value") +namespace MePhoto { + + @summary("Get the current user's profile photo") + @operationId("GetOwnUserPhoto") + @tag("me.photo") + @get + op getOwnUserPhoto(): PhotoRetrieved | OdataError; + + @summary("Update the current user's profile photo") + @operationId("UpdateOwnUserPhotoPut") + @tag("me.photo") + @put + op updateOwnUserPhotoPut( + ...PhotoBody, + ): NoContentSuccess | OdataError; + + @summary("Update the current user's profile photo") + @operationId("UpdateOwnUserPhotoPatch") + @tag("me.photo") + @patch + op updateOwnUserPhotoPatch( + ...PhotoBody, + ): NoContentSuccess | OdataError; + + @summary("Delete the current user's profile photo") + @operationId("DeleteOwnUserPhoto") + @tag("me.photo") + @delete + op deleteOwnUserPhoto(): NoContentSuccess | OdataError; +} + +// ----------------------------------------------------------------------------- +// /v1.0/me/drives +// ----------------------------------------------------------------------------- + +model drivesResponse { + @maxItems(100) + value?: drive[]; + + `@odata.nextLink`?: string; +} + +@route("/v1.0/me/drives") +namespace MeDrives { + + @summary("Get all drives where the current user is a regular member of") + @operationId("ListMyDrives") + @tag("me.drives") + @extension( + "x-ms-pageable", + #{ nextLinkName: "@odata.nextLink", operationName: "listMore" } + ) + @extension("x-ms-docs-operation-type", "operation") + @get + op listMyDrives( + ...OrderBy, + ...DrivesFilter, + ): DrivesRetrieved | OdataError; +} diff --git a/spec/models.tsp b/spec/models.tsp new file mode 100644 index 0000000..704e6fd --- /dev/null +++ b/spec/models.tsp @@ -0,0 +1,244 @@ +import "@typespec/http"; +import "@typespec/openapi"; + +using Http; +using OpenAPI; + +namespace LibreGraph; + +// ----------------------------------------------------------------------------- +// OData error model — referenced from the shared `error` response (default). +// ----------------------------------------------------------------------------- + +@friendlyName("odata.error.detail") +model OdataErrorDetail { + code: string; + message: string; + target?: string; +} + +@friendlyName("odata.error.main") +model OdataErrorMain { + code: string; + message: string; + target?: string; + details?: OdataErrorDetail[]; + + @doc("The structure of this object is service-specific") + innererror?: {}; +} + +@friendlyName("odata.error") +@error +model OdataError { + error: OdataErrorMain; +} + +// ----------------------------------------------------------------------------- +// Identity / IdentitySet +// ----------------------------------------------------------------------------- + +model identity { + @doc("The identity's display name. Note that this may not always be available or up to date. For example, if a user changes their display name, the API may show the new value in a future response, but the items associated with the user won't show up as having changed when using delta.") + displayName: string; + + @doc("Unique identifier for the identity.") + id?: string; + + @doc("The type of the identity. This can be either \"Member\" for regular user, \"Guest\" for guest users or \"Federated\" for users imported from a federated instance. Can be used by clients to indicate the type of user. For more details, clients should look up and cache the user at the /users endpoint.") + `@libre.graph.userType`?: string; +} + +@doc("Optional. User account.") +model identitySet { + @doc("Optional. The application associated with this action.") + application?: identity; + + @doc("Optional. The device associated with this action.") + device?: identity; + + @doc("Optional. The user associated with this action.") + user?: identity; + + @doc("Optional. The group associated with this action.") + group?: identity; +} + +// ----------------------------------------------------------------------------- +// User +// ----------------------------------------------------------------------------- + +@doc("Represents an identity used to sign in to a user account") +model objectIdentity { + @doc("domain of the Provider issuing the identity") + issuer?: string; + + @doc("The unique id assigned by the issuer to the account") + issuerAssignedId?: string; +} + +@doc("Password Profile associated with a user") +model passwordProfile { + @doc("If true the user is required to change their password upon the next login") + forceChangePasswordNextSignIn?: boolean = false; + + @doc("The user's password") + password?: string; +} + +@doc("Provides the last successful sign-in attempt for a user") +model signInActivity { + @doc("The date and time of the last successful sign-in for the user.") + lastSuccessfulSignInDateTime?: utcDateTime; +} + +@doc("Represents the users language setting, ISO-639-1 Code") +scalar language extends string; + +// Stubs for schemas referenced by `userUpdate` but defined in other domains. +// These will be filled in when the corresponding domains are migrated. +model appRoleAssignment {} +model group {} + +@doc("Represents updates to an Active Directory user object.") +model userUpdate { + @doc("Read-only.") +id?: string; + + @doc("Set to \"true\" when the account is enabled.") + accountEnabled?: boolean; + + @doc("The apps and app roles which this user has been assigned.") +appRoleAssignments?: appRoleAssignment[]; + + @doc("The name displayed in the address book for the user. This value is usually the combination of the user's first name, middle initial, and last name. This property is required when a user is created and it cannot be cleared during updates. Returned by default. Supports $orderby.") + displayName?: string; + + @doc("A collection of drives available for this user. Read-only.") +@maxItems(100) + drives?: drive[]; + + @doc("The personal drive of this user. Read-only.") +drive?: drive; + + @doc("Identities associated with this account.") + identities?: objectIdentity[]; + + @doc("The SMTP address for the user, for example, 'jeff@contoso.opencloud.com'. Returned by default.") + mail?: string; + + @doc("Groups that this user is a member of. HTTP Methods: GET (supported for all groups). Read-only. Nullable. Supports $expand.") +memberOf?: group[]; + + @doc("Contains the on-premises SAM account name synchronized from the on-premises directory.") + onPremisesSamAccountName?: string; + + passwordProfile?: passwordProfile; + + @doc("The user's surname (family name or last name). Returned by default.") + surname?: string; + + @doc("The user's givenName. Returned by default.") + givenName?: string; + + @doc("The user`s type. This can be either \"Member\" for regular user, \"Guest\" for guest users or \"Federated\" for users imported from a federated instance.") +userType?: string; + + preferredLanguage?: language; + +signInActivity?: signInActivity; +} + +@doc("Represents an Active Directory user object.") +model user extends userUpdate { + @doc("The name displayed in the address book for the user. This value is usually the combination of the user's first name, middle initial, and last name. This property is required when a user is created and it cannot be cleared during updates. Returned by default. Supports $orderby.") + displayName: string; + + @doc("Contains the on-premises SAM account name synchronized from the on-premises directory.") + onPremisesSamAccountName: string; +} + +// ----------------------------------------------------------------------------- +// Drive +// ----------------------------------------------------------------------------- + +// Stubs — to be filled in when drives/driveItems domain is migrated. +model driveItem {} +model quota {} + +model itemReference { + @doc("Unique identifier of the drive instance that contains the item. Read-only.") +driveId?: string; + + @doc("Identifies the type of drive. See [drive][] resource for values. Read-only.") +driveType?: string; + + @doc("Unique identifier of the item in the drive. Read-only.") +id?: string; + + @doc("The name of the item being referenced. Read-only.") +name?: string; + + @doc("Path that can be used to navigate to the item. Read-only.") +path?: string; +} + +@doc("The drive represents an update to a space on the storage.") +model driveUpdate { + @doc("The unique identifier for this drive.") +id?: string; + + @doc("Identity of the user, device, or application which created the item. Read-only.") +createdBy?: identitySet; + + @doc("Date and time of item creation. Read-only.") +createdDateTime?: utcDateTime; + + @doc("Provides a user-visible description of the item. Optional.") + description?: string; + + @doc("ETag for the item. Read-only.") +eTag?: string; + + @doc("Identity of the user, device, and application which last modified the item. Read-only.") +lastModifiedBy?: identitySet; + + @doc("Date and time the item was last modified. Read-only.") +lastModifiedDateTime?: utcDateTime; + + @doc("The name of the item. Read-write.") + name?: string; + + @doc("Parent information, if the item has a parent. Read-write.") + parentReference?: itemReference; + + @doc("URL that displays the resource in the browser. Read-only.") +webUrl?: string; + + @doc("Describes the type of drive represented by this resource. Values are \"personal\" for users home spaces, \"project\", \"virtual\" or \"share\". Read-only.") +driveType?: string; + + @doc("The drive alias can be used in clients to make the urls user friendly. Example: 'personal/einstein'. This will be used to resolve to the correct driveID.") + driveAlias?: string; + + owner?: identitySet; + quota?: quota; + + @doc("All items contained in the drive. Read-only. Nullable.") +items?: driveItem[]; + + @doc("Drive item describing the drive's root. Read-only.") + root?: driveItem; + + @doc("A collection of special drive resources.") + special?: driveItem[]; + + @doc("Indicates whether the drive has items in the trash. Read-only.") +`@libre.graph.hasTrashedItems`?: boolean; +} + +@doc("The drive represents a space on the storage.") +model drive extends driveUpdate { + @doc("The name of the item. Read-write.") + name: string; +} diff --git a/spec/package-lock.json b/spec/package-lock.json new file mode 100644 index 0000000..179085e --- /dev/null +++ b/spec/package-lock.json @@ -0,0 +1,1171 @@ +{ + "name": "libre-graph-api-spec", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "libre-graph-api-spec", + "version": "0.0.0", + "dependencies": { + "@typespec/compiler": "^1.5.0", + "@typespec/http": "^1.5.0", + "@typespec/openapi": "^1.5.0", + "@typespec/openapi3": "^1.5.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@inquirer/ansi": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.5.tgz", + "integrity": "sha512-doc2sWgJpbFQ64UflSVd17ibMGDuxO1yKgOgLMwavzESnXjFWJqUeG8saYosqKpHp4kWiM5x1nXvEjbpx90gzw==", + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.1.5.tgz", + "integrity": "sha512-Jmf9tgBHIEK5SAOB7swYfStqmtkZb00xOTpSQmkoGEpdxOTpJi9RS0A8bkfDPHTTItZRJrRdZrEMu25wyj0VfQ==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.5", + "@inquirer/core": "^11.1.10", + "@inquirer/figures": "^2.0.5", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "6.0.13", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.0.13.tgz", + "integrity": "sha512-wkGPC7yJ5WJk1DJ5SX7fzk+gfj4BM8cf5dDDi71B/551xHrdsZVRJOC0WyikXd0pEsb/9cLniuE4atbsMqmFkw==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.10", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "11.1.10", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.1.10.tgz", + "integrity": "sha512-a4Q5BXHQAHa9eO202sTaFCHFYVB3x5fauDuThEAdZ9gfn76pSxiKU7wWcEH0N1O0XmQvNfQNU6QXpiRxmYQx+A==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.5", + "@inquirer/figures": "^2.0.5", + "@inquirer/type": "^4.0.5", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.1.2.tgz", + "integrity": "sha512-Y3Nor7S/DhIPo+8Ym/dSY4efwKI4BsflKDwXh0jNeXJsSF3dteS/3Yf+z4wkibVZDvYMyCgknSTQlNahfunGHg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.10", + "@inquirer/external-editor": "^3.0.0", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "5.0.14", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.0.14.tgz", + "integrity": "sha512-qyY9zcIX2eKYwaAUiQo9zORd61Lc3sXeM72fVbeHkYnDkqfr8/armcRbmVAIrExeJhI2puk+uomeKtWrpUVUmQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.10", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-3.0.0.tgz", + "integrity": "sha512-lDSwMgg+M5rq6JKBYaJwSX6T9e/HK2qqZ1oxmOwn4AQoJE5D+7TumsxLGC02PWS//rkIVqbZv3XA3ejsc9FYvg==", + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.5.tgz", + "integrity": "sha512-NsSs4kzfm12lNetHwAn3GEuH317IzpwrMCbOuMIVytpjnJ90YYHNwdRgYGuKmVxwuIqSgqk3M5qqQt1cDk0tGQ==", + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + } + }, + "node_modules/@inquirer/input": { + "version": "5.0.13", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.0.13.tgz", + "integrity": "sha512-0l0jCHlJnXIV8CTxwQC0C+5Ziq8WP22edWgmciW2xYvoeoSck4v5FvCS1ctKdqLLR0dUo93uAHgWHywgBSoRyw==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.10", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.0.13.tgz", + "integrity": "sha512-WHmkYnnJAou5gx7RgcvAfUggnHNM1zWfoh0dFPl3dxVssuqt+dK5rIbaOYQXNyOegvFnopbKupjnhw2O8gANNg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.10", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "5.0.13", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.0.13.tgz", + "integrity": "sha512-XDGu64ROHZjOOXLAANvJN7iIxWKhOSCG5VakrZ5kaScVR+snVJCFglD/hL3/677awtWcu4pXoWa280CDIYcBeg==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.5", + "@inquirer/core": "^11.1.10", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "8.4.3", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.4.3.tgz", + "integrity": "sha512-ai5LseTw9HhegupIgmo4cn7RpnCGznjjXu4OI+7jMR8vu7T1ZCCNMzFFAovUCjL1fl0cceksIN1++yQE59SmZw==", + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^5.1.5", + "@inquirer/confirm": "^6.0.13", + "@inquirer/editor": "^5.1.2", + "@inquirer/expand": "^5.0.14", + "@inquirer/input": "^5.0.13", + "@inquirer/number": "^4.0.13", + "@inquirer/password": "^5.0.13", + "@inquirer/rawlist": "^5.2.9", + "@inquirer/search": "^4.1.9", + "@inquirer/select": "^5.1.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "5.2.9", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.2.9.tgz", + "integrity": "sha512-a1ErXEfgjfPYpyQ89dp+7n2IISjH9oQg3ygvF5adz8B7aHn4n2PjEgu1wpVTp69K3bj3lVLxP0qJ2b1clk1Whw==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.10", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.1.9.tgz", + "integrity": "sha512-ZlbM28Q9lmLkFPNAIv+ZuY530n5Km8U1WW48oYEvDhe9yc2uL3m3t+JSdRUkQlk5fuIuskgiIVjcb7czFzQpuA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.10", + "@inquirer/figures": "^2.0.5", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.1.5.tgz", + "integrity": "sha512-6SRg6kHfK/sjLXOsuqNebuir+sjwrf/iWuRUnXgB2slzEewppI1WfzeS16XxDcOQmXBruMmmB9Cgrz7wsAxqMg==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.5", + "@inquirer/core": "^11.1.10", + "@inquirer/figures": "^2.0.5", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.5.tgz", + "integrity": "sha512-aetVUNeKNc/VriqXlw1NRSW0zhMBB0W4bNbWRJgzRl/3d0QNDQFfk0GO5SDdtjMZVg6o8ZKEiadd7SCCzoOn5Q==", + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@scalar/helpers": { + "version": "0.2.18", + "resolved": "https://registry.npmjs.org/@scalar/helpers/-/helpers-0.2.18.tgz", + "integrity": "sha512-w1d4tpNEVZ293oB2BAgLrS0kVPUtG3eByNmOCJA5eK9vcT4D3cmsGtWjUaaqit0BQCsBFHK51rasGvSWnApYTw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/@scalar/json-magic": { + "version": "0.11.7", + "resolved": "https://registry.npmjs.org/@scalar/json-magic/-/json-magic-0.11.7.tgz", + "integrity": "sha512-GVz9E0vXu+ecypkdn0biK1gbQVkK4QTTX1Hq3eMgxlLQC91wwiqWfCqwfhuX0LRu+Z5OmYhLhufDJEEh56rVgA==", + "license": "MIT", + "dependencies": { + "@scalar/helpers": "0.2.18", + "pathe": "^2.0.3", + "yaml": "^2.8.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@scalar/openapi-parser": { + "version": "0.24.17", + "resolved": "https://registry.npmjs.org/@scalar/openapi-parser/-/openapi-parser-0.24.17.tgz", + "integrity": "sha512-aM9UVrzlMreC3X/sZbyj+7XDZmat3ecGC3RpU8dqEO/HIH+CEX0xMLuP+41DhePCYg5+9TtDomSfWuMq4x1Z1A==", + "license": "MIT", + "dependencies": { + "@scalar/helpers": "0.2.18", + "@scalar/json-magic": "0.11.7", + "@scalar/openapi-types": "0.5.4", + "@scalar/openapi-upgrader": "0.1.11", + "ajv": "^8.17.1", + "ajv-draft-04": "^1.0.0", + "ajv-formats": "^3.0.1", + "jsonpointer": "^5.0.1", + "leven": "^4.0.0", + "yaml": "^2.8.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@scalar/openapi-types": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@scalar/openapi-types/-/openapi-types-0.5.4.tgz", + "integrity": "sha512-2pEbhprh8lLGDfUI6mNm9EV104pjb3+aJsXrFaqfgOSre7r6NlgM5HcSbsLjzDAnTikjJhJ3IMal1Rz8WVwiOw==", + "license": "MIT", + "dependencies": { + "zod": "^4.3.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@scalar/openapi-upgrader": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@scalar/openapi-upgrader/-/openapi-upgrader-0.1.11.tgz", + "integrity": "sha512-ngJcHGoCHmpWgYtNy08vmzFfLdQEkMpvaCQqNPPMNKq0QEXOv89e/rn+TZJZgPnRlY7fDIoIhn9lNgr+azBW+w==", + "license": "MIT", + "dependencies": { + "@scalar/openapi-types": "0.5.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@typespec/asset-emitter": { + "version": "0.79.1", + "resolved": "https://registry.npmjs.org/@typespec/asset-emitter/-/asset-emitter-0.79.1.tgz", + "integrity": "sha512-53s3GLu5BwNkl7Itr/OizfhymTV2u7k5/cwjUOAt03AUDfiKlwbsp+iCIsq1vccJuoDOiXOceJOfL8rAf4/9LQ==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@typespec/compiler": "^1.10.0" + } + }, + "node_modules/@typespec/compiler": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@typespec/compiler/-/compiler-1.12.0.tgz", + "integrity": "sha512-hKCkHEEDdCpXFyOU8ln+TzBBwonFMbkeUV0zIc+vBETyO8p/Upui3XvEyLOyB4CpKUReHzGeGm3gcFjNc73ygg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@inquirer/prompts": "^8.4.1", + "ajv": "^8.18.0", + "change-case": "^5.4.4", + "env-paths": "^4.0.0", + "is-unicode-supported": "^2.1.0", + "mustache": "^4.2.0", + "picocolors": "^1.1.1", + "prettier": "^3.8.1", + "semver": "^7.7.4", + "tar": "^7.5.13", + "temporal-polyfill": "^0.3.2", + "vscode-languageserver": "^9.0.1", + "vscode-languageserver-textdocument": "^1.0.12", + "yaml": "^2.8.3", + "yargs": "^18.0.0" + }, + "bin": { + "tsp": "cmd/tsp.js", + "tsp-server": "cmd/tsp-server.js" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@typespec/http": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@typespec/http/-/http-1.11.0.tgz", + "integrity": "sha512-/DOkN2+MUZyLdmqYmSMZDjxikJTOuNxikTeOwG2fVOibnu8e6S1jzPAuN/mn6YyQBKeBCItMPmUOXIj61Wy8Bg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@typespec/compiler": "^1.11.0", + "@typespec/streams": "^0.81.0" + }, + "peerDependenciesMeta": { + "@typespec/streams": { + "optional": true + } + } + }, + "node_modules/@typespec/openapi": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@typespec/openapi/-/openapi-1.11.0.tgz", + "integrity": "sha512-xUQrHExKBh0XSP4cn+HcondDXjHJM5HCq2Xfy9tB1QflsFh5uP1JJt1+67g73VmHlhZVSUDcoFrnU95pfjyubg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@typespec/compiler": "^1.11.0", + "@typespec/http": "^1.11.0" + } + }, + "node_modules/@typespec/openapi3": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@typespec/openapi3/-/openapi3-1.11.0.tgz", + "integrity": "sha512-AjMpTkIUy8+YlRaqUyQ1NZxbmVbWyT4fPpztVzclyX+TXyvxo9gqLEdqoMfvQ9KUjcU0nsOjfPdaYb5rj25oIA==", + "license": "MIT", + "dependencies": { + "@scalar/json-magic": "^0.11.5", + "@scalar/openapi-parser": "^0.24.1", + "@scalar/openapi-types": "^0.5.0", + "@typespec/asset-emitter": "^0.79.1", + "yaml": "~2.8.2" + }, + "bin": { + "tsp-openapi3": "cmd/tsp-openapi3.js" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@typespec/compiler": "^1.11.0", + "@typespec/events": "^0.81.0", + "@typespec/http": "^1.11.0", + "@typespec/json-schema": "^1.11.0", + "@typespec/openapi": "^1.11.0", + "@typespec/sse": "^0.81.0", + "@typespec/streams": "^0.81.0", + "@typespec/versioning": "^0.81.0" + }, + "peerDependenciesMeta": { + "@typespec/events": { + "optional": true + }, + "@typespec/json-schema": { + "optional": true + }, + "@typespec/sse": { + "optional": true + }, + "@typespec/streams": { + "optional": true + }, + "@typespec/versioning": { + "optional": true + }, + "@typespec/xml": { + "optional": true + } + } + }, + "node_modules/@typespec/openapi3/node_modules/yaml": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.4.tgz", + "integrity": "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "license": "MIT", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/change-case": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", + "license": "MIT" + }, + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "license": "MIT" + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/env-paths": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-4.0.0.tgz", + "integrity": "sha512-pxP8eL2SwwaTRi/KHYwLYXinDs7gL3jxFcBYmEdYfZmZXbaVDvdppd0XBU8qVz03rDfKZMXg1omHCbsJjZrMsw==", + "license": "MIT", + "dependencies": { + "is-safe-filename": "^0.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.0.tgz", + "integrity": "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==", + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/is-safe-filename": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-safe-filename/-/is-safe-filename-0.1.1.tgz", + "integrity": "sha512-4SrR7AdnY11LHfDKTZY1u6Ga3RuxZdl3YKWWShO5iyuG5h8QS4GD2tOb04peBJ5I7pXbR+CGBNEhTcwK+FzN3g==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/leven": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-4.1.0.tgz", + "integrity": "sha512-KZ9W9nWDT7rF7Dazg8xyLHGLrmpgq2nVNFUckhqdW3szVP6YhCpp/RAnpmVExA9JvrMynjwSLVrEj3AepHR6ew==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + } + }, + "node_modules/mute-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/tar": { + "version": "7.5.15", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.15.tgz", + "integrity": "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/temporal-polyfill": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/temporal-polyfill/-/temporal-polyfill-0.3.2.tgz", + "integrity": "sha512-TzHthD/heRK947GNiSu3Y5gSPpeUDH34+LESnfsq8bqpFhsB79HFBX8+Z834IVX68P3EUyRPZK5bL/1fh437Eg==", + "license": "MIT", + "dependencies": { + "temporal-spec": "0.3.1" + } + }, + "node_modules/temporal-spec": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/temporal-spec/-/temporal-spec-0.3.1.tgz", + "integrity": "sha512-B4TUhezh9knfSIMwt7RVggApDRJZo73uZdj8AacL2mZ8RP5KtLianh2MXxL06GN9ESYiIsiuoLQhgVfwe55Yhw==", + "license": "ISC" + }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/spec/package.json b/spec/package.json new file mode 100644 index 0000000..9008b9b --- /dev/null +++ b/spec/package.json @@ -0,0 +1,15 @@ +{ + "name": "libre-graph-api-spec", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "tsp compile ." + }, + "dependencies": { + "@typespec/compiler": "^1.5.0", + "@typespec/http": "^1.5.0", + "@typespec/openapi": "^1.5.0", + "@typespec/openapi3": "^1.5.0" + } +} diff --git a/spec/tspconfig.yaml b/spec/tspconfig.yaml new file mode 100644 index 0000000..6c3fbc3 --- /dev/null +++ b/spec/tspconfig.yaml @@ -0,0 +1,8 @@ +emit: + - "@typespec/openapi3" +options: + "@typespec/openapi3": + emitter-output-dir: "{project-root}/build" + output-file: "v1.0.yaml" + openapi-versions: + - "3.0.0" From 0b20e277da751b905ad9eb6c896220aa9ab11b96 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Tue, 12 May 2026 22:21:36 +0200 Subject: [PATCH 02/45] refactor(spec): apply emitter patterns to me namespace Tightens the me-namespace emit against the existing v1.0.yaml so most remaining oasdiff diffs are resolved: - PATCH /v1.0/me and the photo PUT/PATCH bodies are marked optional so the emitted requestBody matches the existing 'requestBody without required: true' shape - drop descriptions on direct $ref properties (userBase.drive, driveBase.createdBy/lastModifiedBy/parentReference/root, identitySet members). The openapi3 emitter wraps $ref in allOf when a sibling description is present (3.0 disallows sibling annotations on $ref), which oasdiff flags as a structural change - rename the TypeSpec identifiers `user` / `drive` to `userResource` / `driveResource` (with `@friendlyName`) so the auto-named visibility variant of the subtype does not collide with the @friendlyName of its base. This lets us keep the `` extends `Update` allOf relation in the emit - flesh out the appRoleAssignment stub with the required-property set that propagates through ref expansion Known remaining diffs are concentrated on the inline `type: object` / `properties` block that the emitter writes alongside `allOf` when a subtype redeclares an inherited property as required. The resolved schemas are equivalent; these will be revisited once the remaining namespaces are migrated and an end-to-end client-code diff can be run. --- spec/me.tsp | 10 +++--- spec/models.tsp | 90 ++++++++++++++++++++++++++++--------------------- 2 files changed, 57 insertions(+), 43 deletions(-) diff --git a/spec/me.tsp b/spec/me.tsp index 923a139..bc16cae 100644 --- a/spec/me.tsp +++ b/spec/me.tsp @@ -15,13 +15,13 @@ namespace LibreGraph; @doc("Retrieved entity") model UserRetrieved { @statusCode _: 200; - @body body: user; + @body body: userResource; } @doc("Success") model UserSuccess { @statusCode _: 200; - @body body: user; + @body body: userResource; } @doc("Success") @@ -47,7 +47,7 @@ model PhotoBody { @doc("New user photo") @body - body: bytes; + body?: bytes; } // ----------------------------------------------------------------------------- @@ -82,7 +82,7 @@ namespace Me { op updateOwnUser( @doc("New user values") @body - body: userUpdate, + body?: userBase, ): UserSuccess | OdataError; @summary("Get current user") @@ -163,7 +163,7 @@ namespace MePhoto { model drivesResponse { @maxItems(100) - value?: drive[]; + value?: driveResource[]; `@odata.nextLink`?: string; } diff --git a/spec/models.tsp b/spec/models.tsp index 704e6fd..e8b45d1 100644 --- a/spec/models.tsp +++ b/spec/models.tsp @@ -51,16 +51,13 @@ model identity { @doc("Optional. User account.") model identitySet { - @doc("Optional. The application associated with this action.") + // descriptions dropped — direct $ref properties; see userBase.drive note. application?: identity; - @doc("Optional. The device associated with this action.") device?: identity; - @doc("Optional. The user associated with this action.") user?: identity; - @doc("Optional. The group associated with this action.") group?: identity; } @@ -96,30 +93,44 @@ model signInActivity { scalar language extends string; // Stubs for schemas referenced by `userUpdate` but defined in other domains. -// These will be filled in when the corresponding domains are migrated. -model appRoleAssignment {} +// These will be filled in when the corresponding domains are migrated. For +// schemas whose `required` propagates through ref expansion the minimal +// required-property set is declared so oasdiff comparisons reflect the +// existing v1.0.yaml shape. +model appRoleAssignment { + appRoleId: string; + principalId: string; + resourceId: string; +} + model group {} +@friendlyName("userUpdate") @doc("Represents updates to an Active Directory user object.") -model userUpdate { +model userBase { @doc("Read-only.") -id?: string; + id?: string; @doc("Set to \"true\" when the account is enabled.") accountEnabled?: boolean; @doc("The apps and app roles which this user has been assigned.") -appRoleAssignments?: appRoleAssignment[]; + appRoleAssignments?: appRoleAssignment[]; @doc("The name displayed in the address book for the user. This value is usually the combination of the user's first name, middle initial, and last name. This property is required when a user is created and it cannot be cleared during updates. Returned by default. Supports $orderby.") displayName?: string; @doc("A collection of drives available for this user. Read-only.") -@maxItems(100) - drives?: drive[]; + @maxItems(100) + drives?: driveResource[]; - @doc("The personal drive of this user. Read-only.") -drive?: drive; + // NOTE: description (`The personal drive of this user. Read-only.`) is + // dropped because @typespec/openapi3 wraps `$ref` in `allOf` when a sibling + // description is present (OpenAPI 3.0 disallows sibling annotations on + // `$ref`). The existing v1.0.yaml uses the permissive Swagger 2-style + // sibling pattern. To match the existing shape exactly, descriptions on + // direct `$ref` properties are omitted. + drive?: driveResource; @doc("Identities associated with this account.") identities?: objectIdentity[]; @@ -128,7 +139,7 @@ drive?: drive; mail?: string; @doc("Groups that this user is a member of. HTTP Methods: GET (supported for all groups). Read-only. Nullable. Supports $expand.") -memberOf?: group[]; + memberOf?: group[]; @doc("Contains the on-premises SAM account name synchronized from the on-premises directory.") onPremisesSamAccountName?: string; @@ -142,15 +153,16 @@ memberOf?: group[]; givenName?: string; @doc("The user`s type. This can be either \"Member\" for regular user, \"Guest\" for guest users or \"Federated\" for users imported from a federated instance.") -userType?: string; + userType?: string; preferredLanguage?: language; -signInActivity?: signInActivity; + signInActivity?: signInActivity; } +@friendlyName("user") @doc("Represents an Active Directory user object.") -model user extends userUpdate { +model userResource extends userBase { @doc("The name displayed in the address book for the user. This value is usually the combination of the user's first name, middle initial, and last name. This property is required when a user is created and it cannot be cleared during updates. Returned by default. Supports $orderby.") displayName: string; @@ -168,55 +180,56 @@ model quota {} model itemReference { @doc("Unique identifier of the drive instance that contains the item. Read-only.") -driveId?: string; + driveId?: string; @doc("Identifies the type of drive. See [drive][] resource for values. Read-only.") -driveType?: string; + driveType?: string; @doc("Unique identifier of the item in the drive. Read-only.") -id?: string; + id?: string; @doc("The name of the item being referenced. Read-only.") -name?: string; + name?: string; @doc("Path that can be used to navigate to the item. Read-only.") -path?: string; + path?: string; } +@friendlyName("driveUpdate") @doc("The drive represents an update to a space on the storage.") -model driveUpdate { +model driveBase { @doc("The unique identifier for this drive.") -id?: string; + id?: string; - @doc("Identity of the user, device, or application which created the item. Read-only.") -createdBy?: identitySet; + // description dropped — direct $ref, see userBase.drive note. + createdBy?: identitySet; @doc("Date and time of item creation. Read-only.") -createdDateTime?: utcDateTime; + createdDateTime?: utcDateTime; @doc("Provides a user-visible description of the item. Optional.") description?: string; @doc("ETag for the item. Read-only.") -eTag?: string; + eTag?: string; - @doc("Identity of the user, device, and application which last modified the item. Read-only.") -lastModifiedBy?: identitySet; + // description dropped — direct $ref, see userBase.drive note. + lastModifiedBy?: identitySet; @doc("Date and time the item was last modified. Read-only.") -lastModifiedDateTime?: utcDateTime; + lastModifiedDateTime?: utcDateTime; @doc("The name of the item. Read-write.") name?: string; - @doc("Parent information, if the item has a parent. Read-write.") + // description dropped — direct $ref, see userBase.drive note. parentReference?: itemReference; @doc("URL that displays the resource in the browser. Read-only.") -webUrl?: string; + webUrl?: string; @doc("Describes the type of drive represented by this resource. Values are \"personal\" for users home spaces, \"project\", \"virtual\" or \"share\". Read-only.") -driveType?: string; + driveType?: string; @doc("The drive alias can be used in clients to make the urls user friendly. Example: 'personal/einstein'. This will be used to resolve to the correct driveID.") driveAlias?: string; @@ -225,20 +238,21 @@ driveType?: string; quota?: quota; @doc("All items contained in the drive. Read-only. Nullable.") -items?: driveItem[]; + items?: driveItem[]; - @doc("Drive item describing the drive's root. Read-only.") + // description dropped — direct $ref, see userBase.drive note. root?: driveItem; @doc("A collection of special drive resources.") special?: driveItem[]; @doc("Indicates whether the drive has items in the trash. Read-only.") -`@libre.graph.hasTrashedItems`?: boolean; + `@libre.graph.hasTrashedItems`?: boolean; } +@friendlyName("drive") @doc("The drive represents a space on the storage.") -model drive extends driveUpdate { +model driveResource extends driveBase { @doc("The name of the item. Read-write.") name: string; } From b4e56b96d9b18bcd419b8427f0e4f39ee8b2de1b Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Tue, 12 May 2026 22:26:14 +0200 Subject: [PATCH 03/45] feat(spec): add drives namespace with full driveItem schema Covers the v1.0 drives namespace operations: - GET /v1.0/drives (ListAllDrives) - POST /v1.0/drives (CreateDrive) - GET /v1.0/drives/{drive-id} (GetDrive) - PATCH /v1.0/drives/{drive-id} (UpdateDrive) - DELETE /v1.0/drives/{drive-id} (DeleteDrive) - GET /v1beta1/drives (ListAllDrivesBeta) The driveItem schema and all 17 facet schemas are fully translated: deleted, openGraphFile, fileSystemInfo, folderView, folder, image, photo, motionPhoto, geoCoordinates, thumbnail, thumbnailSet, root, trash, specialFolder, audio, video, remoteItem; plus hashes, permission and sharingInvitation referenced from the facet chain. quota gets the proper definition (replacing the stub in models.tsp). Notes: - bytes properties marked with `@encode("base64url")` so the emitter produces `format: base64url` (the existing v1.0.yaml shape) rather than the TypeSpec default `format: byte` - sharePointIdentitySet and sharingLink remain as minimal stubs in models.tsp until the sharing/permissions operations are migrated - drives/{drive-id}/root and drives/{drive-id}/items/* operations (mostly the v1beta1 surface) are not yet covered --- spec/drives.tsp | 677 ++++++++++++++++++++++++++++++++++++++++++++++++ spec/main.tsp | 1 + spec/models.tsp | 24 +- 3 files changed, 695 insertions(+), 7 deletions(-) create mode 100644 spec/drives.tsp diff --git a/spec/drives.tsp b/spec/drives.tsp new file mode 100644 index 0000000..54942ed --- /dev/null +++ b/spec/drives.tsp @@ -0,0 +1,677 @@ +import "@typespec/http"; +import "@typespec/openapi"; + +import "./models.tsp"; + +using Http; +using OpenAPI; + +namespace LibreGraph; + +// ============================================================================= +// driveItem facet schemas +// ============================================================================= + +@doc("Information about the deleted state of the item. Read-only.") +model deleted { + @doc("Represents the state of the deleted item.") + state?: string; +} + +@doc("Hashes of the file's binary content, if available. Read-only.") +model hashes { + @doc("The CRC32 value of the file (if available). Read-only.") + @maxLength(8) + crc32Hash?: string; + + @doc("A proprietary hash of the file that can be used to determine if the contents of the file have changed (if available). Read-only.") + quickXorHash?: string; + + @doc("SHA1 hash for the contents of the file (if available). Read-only.") + @maxLength(40) + sha1Hash?: string; + + @doc("SHA256 hash for the contents of the file (if available). Read-only.") + @maxLength(160) + sha256Hash?: string; +} + +@doc("File metadata, if the item is a file. Read-only.") +model openGraphFile { + hashes?: hashes; + + @doc("The MIME type for the file. This is determined by logic on the server and might not be the value provided when the file was uploaded. Read-only.") + mimeType?: string; + + processingMetadata?: boolean; +} + +@doc("File system information on client. Read-write.") +model fileSystemInfo { + @doc("The UTC date and time the file was created on a client.") + createdDateTime?: utcDateTime; + + @doc("The UTC date and time the file was last accessed. Available for the recent file list only.") + lastAccessedDateTime?: utcDateTime; + + @doc("The UTC date and time the file was last modified on a client.") + lastModifiedDateTime?: utcDateTime; +} + +@doc("A collection of properties defining the recommended view for the folder.") +model folderView { + @doc("The method by which the folder should be sorted.") + sortBy?: string; + + @doc("If true, indicates that items should be sorted in descending order. Otherwise, items should be sorted ascending.") + sortOrder?: string; + + @doc("The type of view that should be used to represent the folder.") + viewType?: string; +} + +@doc("Folder metadata, if the item is a folder. Read-only.") +model folder { + @doc("Number of children contained immediately within this container.") + @maxValue(2147483647) + childCount?: int32; + + view?: folderView; +} + +@doc("Image metadata, if the item is an image. Read-only.") +model image { + @doc("Optional. Height of the image, in pixels. Read-only.") + @maxValue(2147483647) + height?: int32; + + @doc("Optional. Width of the image, in pixels. Read-only.") + @maxValue(2147483647) + width?: int32; +} + +@doc(""" + The photo resource provides photo and camera properties, for example, EXIF metadata, on a driveItem. + """) +model photo { + @doc("Camera manufacturer. Read-only.") + cameraMake?: string; + + @doc("Camera model. Read-only.") + cameraModel?: string; + + @doc("The denominator for the exposure time fraction from the camera. Read-only.") + exposureDenominator?: float64; + + @doc("The numerator for the exposure time fraction from the camera. Read-only.") + exposureNumerator?: float64; + + @doc("The F-stop value from the camera. Read-only.") + fNumber?: float64; + + @doc("The focal length from the camera. Read-only.") + focalLength?: float64; + + @doc("The ISO value from the camera. Read-only.") + iso?: int32; + + @doc("The orientation value from the camera. Read-only.") + orientation?: int16; + + @doc("Represents the date and time the photo was taken. Read-only.") + takenDateTime?: utcDateTime; +} + +@doc(""" + Motion Photo metadata. A Motion Photo is a still image with a short video clip appended to + the end of the file. The presence of this facet on a driveItem indicates that the item is + a Motion Photo; absence indicates it is not. + + Based on the Google Motion Photo format v1.0 specification: + https://developer.android.com/media/platform/motion-photo-format + """) +model motionPhoto { + @doc("The file format version of the Motion Photo. Currently always 1. Read-only.") + version?: int32; + + @doc(""" + Presentation timestamp in microseconds of the video frame that corresponds to the still + image. A value of -1 indicates unspecified. If absent, readers should use a timestamp + near the middle of the video track. Read-only. + """) + presentationTimestampUs?: int64; + + @doc(""" + Size in bytes of the embedded video portion of the file. The video is appended at the + end of the file, so clients can fetch it with a Range request: + `Range: bytes=-`. Read-only. + """) + videoSize?: int64; +} + +@doc(""" + The GeoCoordinates resource provides geographic coordinates and elevation of a location based on metadata contained within the file. + If a DriveItem has a non-null location facet, the item represents a file with a known location associated with it. + """) +model geoCoordinates { + @doc("The altitude (height), in feet, above sea level for the item. Read-only.") + altitude?: float64; + + @doc("The latitude, in decimal, for the item. Read-only.") + latitude?: float64; + + @doc("The longitude, in decimal, for the item. Read-only.") + longitude?: float64; +} + +@doc(""" + The thumbnail resource type represents a thumbnail for an image, video, document, or any item that has a bitmap representation. + """) +model thumbnail { + @doc("The content stream for the thumbnail.") + @encode("base64url") + content?: bytes; + + @doc("The height of the thumbnail, in pixels.") + height?: int32; + + @doc("The unique identifier of the item that provided the thumbnail. This is only available when a folder thumbnail is requested.") + sourceItemId?: string; + + @doc("The URL used to fetch the thumbnail content.") + url?: string; + + @doc("The width of the thumbnail, in pixels.") + width?: int32; +} + +@doc(""" + The ThumbnailSet resource is a keyed collection of thumbnail resources. + It's used to represent a set of thumbnails associated with a DriveItem. + """) +model thumbnailSet { + @doc("The ID within the item. Read-only.") + id?: string; + + // description dropped — direct $ref, see userBase.drive note. + large?: thumbnail; + + // description dropped — direct $ref, see userBase.drive note. + medium?: thumbnail; + + // description dropped — direct $ref, see userBase.drive note. + small?: thumbnail; + + // description dropped — direct $ref, see userBase.drive note. + source?: thumbnail; +} + +@doc("If this property is non-null, it indicates that the driveItem is the top-most driveItem in the drive.") +model root {} + +@doc("Metadata for trashed drive Items") +model trash { + // description dropped — direct $ref, see userBase.drive note. + trashedBy?: identitySet; + + @doc("The UTC date and time the folder was marked as trashed.") + trashedDateTime?: utcDateTime; +} + +@doc("If the current item is also available as a special folder, this facet is returned. Read-only") +model specialFolder { + @doc("The unique identifier for this item in the /drive/special collection") + name?: string; +} + +@doc(""" + The Audio resource groups audio-related properties on an item into a single structure. + + If a DriveItem has a non-null audio facet, the item represents an audio file. The properties of the Audio resource are populated by extracting metadata from the file. + """) +model audio { + @doc("The title of the album for this audio file.") + album?: string; + + @doc("The artist named on the album for the audio file.") + albumArtist?: string; + + @doc("The performing artist for the audio file.") + artist?: string; + + @doc("Bitrate expressed in kbps.") + bitrate?: int64; + + @doc("The name of the composer of the audio file.") + composers?: string; + + @doc("Copyright information for the audio file.") + copyright?: string; + + @doc("The number of the disc this audio file came from.") + disc?: int16; + + @doc("The total number of discs in this album.") + discCount?: int16; + + @doc("Duration of the audio file, expressed in milliseconds") + duration?: int64; + + @doc("The genre of this audio file.") + genre?: string; + + @doc("Indicates if the file is protected with digital rights management.") + hasDrm?: boolean; + + @doc("Indicates if the file is encoded with a variable bitrate.") + isVariableBitrate?: boolean; + + @doc("The title of the audio file.") + title?: string; + + @doc("The number of the track on the original disc for this audio file.") + track?: int32; + + @doc("The total number of tracks on the original disc for this audio file.") + trackCount?: int32; + + @doc("The year the audio file was recorded.") + year?: int32; +} + +@doc(""" + The video resource groups video-related data items into a single structure. + + If a driveItem has a non-null video facet, the item represents a video file. The properties of the video resource are populated by extracting metadata from the file. + """) +model video { + @doc("Number of audio bits per sample.") + audioBitsPerSample?: int32; + + @doc("Number of audio channels.") + audioChannels?: int32; + + @doc("Name of the audio format (AAC, MP3, etc.).") + audioFormat?: string; + + @doc("Number of audio samples per second.") + audioSamplesPerSecond?: int32; + + @doc("Bit rate of the video in bits per second.") + bitrate?: int32; + + @doc("Duration of the file in milliseconds.") + duration?: int64; + + @doc("\"Four character code\" name of the video format.") + fourCC?: string; + + @doc("Frame rate of the video.") + frameRate?: float64; + + @doc("Height of the video, in pixels.") + height?: int32; + + @doc("Width of the video, in pixels.") + width?: int32; +} + +@doc("invitation-related data items") +model sharingInvitation { + // description dropped — direct $ref, see userBase.drive note. + invitedBy?: identitySet; +} + +@doc(""" + The Permission resource provides information about a sharing permission granted for a DriveItem resource. + + ### Remarks + + The Permission resource uses *facets* to provide information about the kind of permission represented by the resource. + + Permissions with a `link` facet represent sharing links created on the item. Sharing links contain a unique token that provides access to the item for anyone with the link. + + Permissions with a `invitation` facet represent permissions added by inviting specific users or groups to have access to the file. + """) +model permission { + @doc("The unique identifier of the permission among all permissions on the item. Read-only.") + id?: string; + + @doc(""" + Indicates whether the password is set for this permission. This property only + appears in the response. Optional. Read-only. + """) + hasPassword?: boolean; + + @doc("An optional expiration date which limits the permission in time.") + expirationDateTime?: utcDateTime | null; + + @doc("An optional creation date. Libregraph only.") + createdDateTime?: utcDateTime | null; + + // description dropped — direct $ref, see userBase.drive note. + grantedToV2?: sharePointIdentitySet; + + // description dropped — direct $ref, see userBase.drive note. + link?: sharingLink; + + roles?: string[]; + + // description dropped — direct $ref array, only carries through. + @doc("For link type permissions, the details of the identity to whom permission was granted. This could be used to grant access to a an external user that can be identified by email, aka guest accounts.") + grantedToIdentities?: identitySet[]; + + @doc("Use this to create a permission with custom actions.") + `@libre.graph.permissions.actions`?: string[]; + + // description dropped — direct $ref, see userBase.drive note. + invitation?: sharingInvitation; +} + +@doc("Remote item data, if the item is shared from a drive other than the one being accessed. Read-only.") +@friendlyName("remoteItem") +model remoteItemModel { + createdBy?: identitySet; + + @doc("Date and time of item creation. Read-only.") + createdDateTime?: utcDateTime; + + file?: openGraphFile; + fileSystemInfo?: fileSystemInfo; + folder?: folder; + + @doc("The drive alias can be used in clients to make the urls user friendly. Example: 'personal/einstein'. This will be used to resolve to the correct driveID.") + driveAlias?: string; + + @doc("The relative path of the item in relation to its drive root.") + path?: string; + + @doc("Unique identifier for the drive root of this item. Read-only.") + rootId?: string; + + @doc("Unique identifier for the remote item in its drive. Read-only.") + id?: string; + + image?: image; + lastModifiedBy?: identitySet; + + @doc("Date and time the item was last modified. Read-only.") + lastModifiedDateTime?: utcDateTime; + + @doc("Optional. Filename of the remote item. Read-only.") + name?: string; + + @doc("ETag for the item. Read-only.") + eTag?: string; + + @doc("An eTag for the content of the item. This eTag is not changed if only the metadata is changed. Note This property is not returned if the item is a folder. Read-only.") + cTag?: string; + + parentReference?: itemReference; + + @doc("The set of permissions for the item. Read-only. Nullable.") + permissions?: permission[]; + + @doc("Size of the remote item. Read-only.") + size?: int64; + + specialFolder?: specialFolder; + + @doc("DAV compatible URL for the item.") + webDavUrl?: string; + + @doc("URL that displays the resource in the browser. Read-only.") + webUrl?: string; +} + +// ============================================================================= +// quota — replaces the stub in models.tsp +// ============================================================================= + +@friendlyName("quota") +@doc("Optional. Information about the drive's storage space quota. Read-only.") +model quotaModel { + @doc("Total space consumed by files in the recycle bin, in bytes. Read-only.") + deleted?: int64; + + @doc("Total space remaining before reaching the quota limit, in bytes. Read-only.") + remaining?: int64; + + @doc("Enumeration value that indicates the state of the storage space. Either \"normal\", \"nearing\", \"critical\" or \"exceeded\". Read-only.") + state?: string; + + @doc("Total allowed storage space, in bytes. Read-only.") + total?: int64; + + @doc("Total space used, in bytes. Read-only.") + used?: int64; +} + +// ============================================================================= +// driveItem — full schema. Replaces the stub in models.tsp. +// ============================================================================= + +@friendlyName("driveItem") +@doc("Represents a resource inside a drive. Read-only.") +model driveItemModel { + @doc("Read-only.") + id?: string; + + // description dropped — direct $ref, see userBase.drive note. + createdBy?: identitySet; + + @doc("Date and time of item creation. Read-only.") + createdDateTime?: utcDateTime; + + @doc("Provides a user-visible description of the item. Optional.") + description?: string; + + @doc("ETag for the item. Read-only.") + eTag?: string; + + // description dropped — direct $ref, see userBase.drive note. + lastModifiedBy?: identitySet; + + @doc("Date and time the item was last modified. Read-only.") + lastModifiedDateTime?: utcDateTime; + + @doc("The name of the item. Read-write.") + name?: string; + + // description dropped — direct $ref, see userBase.drive note. + parentReference?: itemReference; + + @doc("URL that displays the resource in the browser. Read-only.") + webUrl?: string; + + @doc("The content stream, if the item represents a file.") + @encode("base64url") + content?: bytes; + + @doc("An eTag for the content of the item. This eTag is not changed if only the metadata is changed. Note This property is not returned if the item is a folder. Read-only.") + cTag?: string; + + deleted?: deleted; + file?: openGraphFile; + fileSystemInfo?: fileSystemInfo; + folder?: folder; + image?: image; + photo?: photo; + location?: geoCoordinates; + + @doc("Collection containing ThumbnailSet objects associated with the item. Read-only. Nullable.") + thumbnails?: thumbnailSet[]; + + root?: root; + trash?: trash; + specialFolder?: specialFolder; + remoteItem?: remoteItemModel; + + @doc("Size of the item in bytes. Read-only.") + size?: int64; + + @doc("WebDAV compatible URL for the item. Read-only.") + webDavUrl?: string; + + @doc("Collection containing Item objects for the immediate children of Item. Only items representing folders have children. Read-only. Nullable.") + children?: driveItemModel[]; + + @doc("The set of permissions for the item. Read-only. Nullable.") + permissions?: permission[]; + + audio?: audio; + video?: video; + `@libre.graph.motionPhoto`?: motionPhoto; + + @doc("Indicates if the item is synchronized with the underlying storage provider. Read-only.") + `@client.synchronize`?: boolean; + + @doc(""" + A pre-authenticated URL that can be used to download the item's content without + providing an Authorization header. The URL is short-lived and cannot be cached. + + This annotation is only populated when explicitly requested via `$select`, and + only for items that have a `file` facet. The returned URL is valid for a + limited time and should be used promptly. + """) + `@microsoft.graph.downloadUrl`?: string; + + @doc("Properties or facets (see UI.Facet) annotated with this term will not be rendered if the annotation evaluates to true. Users can set this to hide permissions.") + `@UI.Hidden`?: boolean; +} + +// ============================================================================= +// Shared parameters used by drives operations +// ============================================================================= + +@friendlyName("drivesSelect") +model DrivesSelect { + @query(#{ explode: false }) + @doc("Select properties to be returned. By default all properties are returned.") + `$select`?: "@libre.graph.hasTrashedItems"[]; +} + +@friendlyName("drivesExpand") +model DrivesExpand { + @query + @doc("Expand related entities") + `$expand`?: string; +} + +model DriveIdPath { + @path + @extension("x-ms-docs-key-type", "drive") + @doc("key: id of drive") + `drive-id`: string; +} + +// ============================================================================= +// Drives operations +// ============================================================================= + +@doc("Retrieved spaces") +model DrivesListResponse { + @statusCode _: 200; + @body body: drivesResponse; +} + +@doc("Retrieved drive") +model DriveRetrieved { + @statusCode _: 200; + @body body: driveResource; +} + +@doc("Created") +model DriveCreated { + @statusCode _: 201; + @body body: driveResource; +} + +@doc("Success") +model DriveUpdated { + @statusCode _: 200; + @body body: driveResource; +} + +@route("/v1.0/drives") +namespace DrivesAll { + + @summary("Get all available drives") + @operationId("ListAllDrives") + @tag("drives.GetDrives") + @extension("x-ms-docs-operation-type", "operation") + @get + op listAllDrives( + ...OrderBy, + ...DrivesFilter, + ): DrivesListResponse | OdataError; + + @summary("Create a new drive of a specific type") + @operationId("CreateDrive") + @tag("drives") + @extension("x-ms-docs-operation-type", "operation") + @post + op createDrive( + @doc("New space property values") + @body + body: driveResource, + ): DriveCreated | OdataError; +} + +@route("/v1.0/drives/{drive-id}") +namespace Drive { + + @summary("Get drive by id") + @operationId("GetDrive") + @tag("drives") + @extension("x-ms-docs-operation-type", "operation") + @get + op getDrive( + ...DriveIdPath, + ...DrivesSelect, + ): DriveRetrieved | OdataError; + + @summary("Update the drive") + @operationId("UpdateDrive") + @tag("drives") + @extension("x-ms-docs-operation-type", "operation") + @patch + op updateDrive( + ...DriveIdPath, + @doc("New space values") + @body + body: driveBase, + ): DriveUpdated | OdataError; + + @summary("Delete a specific space") + @operationId("DeleteDrive") + @tag("drives") + @extension("x-ms-docs-operation-type", "operation") + @delete + op deleteDrive( + ...DriveIdPath, + @header("If-Match") + @doc("ETag") + ifMatch?: string, + ): NoContentSuccess | OdataError; +} + +// ============================================================================= +// /v1beta1/drives — beta alias with extra query parameters +// ============================================================================= + +@route("/v1beta1/drives") +namespace DrivesBeta { + + @summary("Alias for '/v1.0/drives', the difference is that grantedtoV2 is used and roles contain unified roles instead of cs3 roles") + @operationId("ListAllDrivesBeta") + @tag("drives.GetDrives") + @extension("x-ms-docs-operation-type", "operation") + @get + op listAllDrivesBeta( + ...OrderBy, + ...DrivesFilter, + ...DrivesExpand, + ...DrivesSelect, + ): DrivesListResponse | OdataError; +} diff --git a/spec/main.tsp b/spec/main.tsp index ab3b3b9..fb34a17 100644 --- a/spec/main.tsp +++ b/spec/main.tsp @@ -4,6 +4,7 @@ import "@typespec/openapi3"; import "./models.tsp"; import "./me.tsp"; +import "./drives.tsp"; using Http; using OpenAPI; diff --git a/spec/models.tsp b/spec/models.tsp index e8b45d1..1f48e17 100644 --- a/spec/models.tsp +++ b/spec/models.tsp @@ -98,8 +98,13 @@ scalar language extends string; // required-property set is declared so oasdiff comparisons reflect the // existing v1.0.yaml shape. model appRoleAssignment { + @format("uuid") appRoleId: string; + + @format("uuid") principalId: string; + + @format("uuid") resourceId: string; } @@ -174,9 +179,14 @@ model userResource extends userBase { // Drive // ----------------------------------------------------------------------------- -// Stubs — to be filled in when drives/driveItems domain is migrated. -model driveItem {} -model quota {} +// driveItem, quota and the driveItem facet schemas are defined in `drives.tsp`. +// Stubs for shared schemas defined in other (not-yet-migrated) domains: +model sharePointIdentitySet { + user?: identity; + group?: identity; +} + +model sharingLink {} model itemReference { @doc("Unique identifier of the drive instance that contains the item. Read-only.") @@ -235,16 +245,16 @@ model driveBase { driveAlias?: string; owner?: identitySet; - quota?: quota; + quota?: quotaModel; @doc("All items contained in the drive. Read-only. Nullable.") - items?: driveItem[]; + items?: driveItemModel[]; // description dropped — direct $ref, see userBase.drive note. - root?: driveItem; + root?: driveItemModel; @doc("A collection of special drive resources.") - special?: driveItem[]; + special?: driveItemModel[]; @doc("Indicates whether the drive has items in the trash. Read-only.") `@libre.graph.hasTrashedItems`?: boolean; From c154ec8c6400af4746fdee56ef9a8669c6181b56 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Tue, 12 May 2026 22:42:17 +0200 Subject: [PATCH 04/45] feat(spec): cover me/drive convenience routes + v1beta1 driveItem ops Adds the remaining v1.0 me/drive helper routes and the basic v1beta1 driveItem CRUD surface: - GET /v1.0/me/drive (GetHome) - GET /v1.0/me/drive/root (HomeGetRoot) - GET /v1.0/me/drive/root/children (HomeGetChildren) - GET /v1beta1/me/drive/sharedByMe (ListSharedByMe) - GET /v1beta1/me/drive/sharedWithMe (ListSharedWithMe) - POST /v1.0/me/drive/items/{item-id}/follow (FollowDriveItem) - DELETE /v1.0/me/drive/following/{item-id} (UnfollowDriveItem) - GET /v1beta1/me/drives (ListMyDrivesBeta) - GET /v1.0/drives/{drive-id}/root (GetRoot) - GET /v1beta1/drives/{drive-id}/items/{item-id} (GetDriveItem) - PATCH /v1beta1/drives/{drive-id}/items/{item-id} (UpdateDriveItem) - DELETE /v1beta1/drives/{drive-id}/items/{item-id} (DeleteDriveItem) - GET /v1beta1/drives/{drive-id}/items/{item-id}/content (GetDriveItemContent) The content download op uses a 302 redirect response with a Location header, which TypeSpec models cleanly via a status-coded response model. The shared item-id path parameter is factored as a reusable spread. Sharing/permissions/invitations on drives and driveItems are not yet covered. --- spec/drives.tsp | 160 ++++++++++++++++++++++++++++++++++++++++++++++++ spec/me.tsp | 159 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 319 insertions(+) diff --git a/spec/drives.tsp b/spec/drives.tsp index 54942ed..27cd2b6 100644 --- a/spec/drives.tsp +++ b/spec/drives.tsp @@ -551,6 +551,20 @@ model DrivesSelect { `$select`?: "@libre.graph.hasTrashedItems"[]; } +@friendlyName("driveItemSelect") +model DriveItemSelect { + @query(#{ explode: false }) + @doc("Select additional properties to be returned.") + `$select`?: "@microsoft.graph.downloadUrl"[]; +} + +model ItemIdPath { + @path + @extension("x-ms-docs-key-type", "item") + @doc("key: id of item") + `item-id`: string; +} + @friendlyName("drivesExpand") model DrivesExpand { @query @@ -675,3 +689,149 @@ namespace DrivesBeta { ...DrivesSelect, ): DrivesListResponse | OdataError; } + +// ============================================================================= +// /v1.0/drives/{drive-id}/root +// ============================================================================= + +@doc("Retrieved resource") +model DriveItemRetrieved { + @statusCode _: 200; + @body body: driveItemModel; +} + +@route("/v1.0/drives/{drive-id}/root") +namespace DriveRoot { + + @summary("Get root from arbitrary space") + @operationId("GetRoot") + @tag("drives.root") + @extension("x-ms-docs-operation-type", "operation") + @get + op getRoot(...DriveIdPath): DriveItemRetrieved | OdataError; +} + +// ============================================================================= +// /v1beta1/drives/{drive-id}/items/{item-id} — driveItem operations +// ============================================================================= + +@doc("Retrieved driveItem") +model DriveItemRetrievedSelected { + @statusCode _: 200; + @body body: driveItemModel; +} + +@doc("Success") +model DriveItemUpdated { + @statusCode _: 200; + @body body: driveItemModel; +} + +@route("/v1beta1/drives/{drive-id}/items/{item-id}") +namespace DriveItem { + + @summary("Get a DriveItem.") + @doc("Get a DriveItem by using its ID.") + @operationId("GetDriveItem") + @tag("driveItem") + @extension("x-ms-docs-operation-type", "operation") + @get + op getDriveItem( + ...DriveIdPath, + ...ItemIdPath, + ...DriveItemSelect, + ): DriveItemRetrievedSelected | OdataError; + + @summary("Update a DriveItem.") + @doc(""" + Update a DriveItem. + + The request body must include a JSON object with the properties to update. + Only the properties that are provided will be updated. + + Currently it supports updating the following properties: + + * `@UI.Hidden` - Hides the item from the UI. + """) + @operationId("UpdateDriveItem") + @tag("driveItem") + @extension("x-ms-docs-operation-type", "operation") + @patch + op updateDriveItem( + ...DriveIdPath, + ...ItemIdPath, + @doc("DriveItem properties to update") + @body + body: driveItemModel, + ): DriveItemUpdated | OdataError; + + @summary("Delete a DriveItem.") + @doc(""" + Delete a DriveItem by using its ID. + + Deleting items using this method moves the items to the recycle bin instead of permanently deleting the item. + + Mounted shares in the share jail are unmounted. The `@client.synchronize` property of the `driveItem` in the [sharedWithMe](#/me.drive/ListSharedWithMe) endpoint will change to false. + """) + @operationId("DeleteDriveItem") + @tag("driveItem") + @extension("x-ms-docs-operation-type", "operation") + @delete + op deleteDriveItem( + ...DriveIdPath, + ...ItemIdPath, + ): NoContentSuccess | OdataError; +} + +// ============================================================================= +// /v1beta1/drives/{drive-id}/items/{item-id}/content — content download +// ============================================================================= + +@doc("Pre-authenticated redirect to the file content.") +model DriveItemContentRedirect { + @statusCode _: 302; + + @header("Location") + @doc("The pre-authenticated URL where the content can be downloaded.") + location: url; +} + +@doc("The driveItem was not found or is not a file.") +@error +model DriveItemContentNotFound { + @statusCode _: 404; + @body body: OdataError; +} + +@route("/v1beta1/drives/{drive-id}/items/{item-id}/content") +namespace DriveItemContent { + + @summary("Download the content of a DriveItem") + @doc(""" + Download the contents of the primary stream (file) of a driveItem. Only + driveItem objects with a `file` facet can be downloaded. + + The response is a `302 Found` redirecting to a pre-authenticated download + URL for the file. This is the same URL that is returned via the + `@microsoft.graph.downloadUrl` instance annotation on the driveItem when + requested via `$select`. Choose between the two based on whether you + want to call the redirecting `/content` endpoint directly (for example, + with a client that follows redirects automatically) or you want to + inspect / schedule / prefetch the URL yourself via the annotation. + + The pre-authenticated URL is short-lived and does not require an + `Authorization` header. + + To download a partial range of bytes, apply the `Range` header to the + redirect target (the pre-authenticated URL), not to the `/content` + request. + """) + @operationId("GetDriveItemContent") + @tag("driveItem") + @extension("x-ms-docs-operation-type", "operation") + @get + op getDriveItemContent( + ...DriveIdPath, + ...ItemIdPath, + ): DriveItemContentRedirect | DriveItemContentNotFound | OdataError; +} diff --git a/spec/me.tsp b/spec/me.tsp index bc16cae..4243ca9 100644 --- a/spec/me.tsp +++ b/spec/me.tsp @@ -185,3 +185,162 @@ namespace MeDrives { ...DrivesFilter, ): DrivesRetrieved | OdataError; } + +@route("/v1beta1/me/drives") +namespace MeDrivesBeta { + + @summary("Alias for '/v1.0/drives', the difference is that grantedtoV2 is used and roles contain unified roles instead of cs3 roles") + @operationId("ListMyDrivesBeta") + @tag("me.drives") + @extension( + "x-ms-pageable", + #{ nextLinkName: "@odata.nextLink", operationName: "listMore" } + ) + @extension("x-ms-docs-operation-type", "operation") + @get + op listMyDrivesBeta( + ...OrderBy, + ...DrivesFilter, + ...DrivesExpand, + ...DrivesSelect, + ): DrivesRetrieved | OdataError; +} + +// ----------------------------------------------------------------------------- +// /v1.0/me/drive — personal drive convenience routes +// ----------------------------------------------------------------------------- + +@doc("Retrieved personal space") +model PersonalDriveRetrieved { + @statusCode _: 200; + @body body: driveResource; +} + +@doc("Retrieved resource") +model MeDriveItemRetrieved { + @statusCode _: 200; + @body body: driveItemModel; +} + +@doc("Retrieved resource list") +model MeDriveItemListRetrieved { + @statusCode _: 200; + @body body: meDriveChildrenResponse; +} + +@doc("OK") +model SharedItemsRetrieved { + @statusCode _: 200; + @body body: sharedDriveItemsResponse; +} + +@doc("Created") +model FollowedDriveItemCreated { + @statusCode _: 201; + @body body: driveItemModel; +} + +@doc("No Content") +model UnfollowedNoContent { + @statusCode _: 204; +} + +model meDriveChildrenResponse { + @maxItems(100) + value?: driveItemModel[]; + + `@odata.nextLink`?: string; +} + +model sharedDriveItemsResponse { + value?: driveItemModel[]; +} + +@route("/v1.0/me/drive") +namespace MeDrive { + + @summary("Get personal space for user") + @operationId("GetHome") + @tag("me.drive") + @extension("x-ms-docs-operation-type", "operation") + @get + op getHome(): PersonalDriveRetrieved | OdataError; +} + +@route("/v1.0/me/drive/root") +namespace MeDriveRoot { + + @summary("Get root from personal space") + @operationId("HomeGetRoot") + @tag("me.drive.root") + @extension("x-ms-docs-operation-type", "operation") + @get + op homeGetRoot(): MeDriveItemRetrieved | OdataError; +} + +@route("/v1.0/me/drive/root/children") +namespace MeDriveRootChildren { + + @summary("Get children from drive") + @operationId("HomeGetChildren") + @tag("me.drive.root.children") + @extension("x-ms-docs-operation-type", "operation") + @get + op homeGetChildren(): MeDriveItemListRetrieved | OdataError; +} + +@route("/v1beta1/me/drive/sharedByMe") +namespace MeDriveSharedByMe { + + @summary("Get a list of driveItem objects shared by the current user.") + @doc("The `driveItems` returned from the `sharedByMe` method always include the `permissions` relation that indicates they are shared items.") + @operationId("ListSharedByMe") + @tag("me.drive") + @extension("x-ms-docs-operation-type", "operation") + @get + op listSharedByMe( + @doc("Expand related entities") + @query(#{ explode: false }) + `$expand`?: "thumbnails"[], + ): SharedItemsRetrieved | OdataError; +} + +@route("/v1beta1/me/drive/sharedWithMe") +namespace MeDriveSharedWithMe { + + @summary("Get a list of driveItem objects shared with the owner of a drive.") + @doc("The `driveItems` returned from the `sharedWithMe` method always include the `remoteItem` facet that indicates they are items from a different drive.") + @operationId("ListSharedWithMe") + @tag("me.drive") + @extension("x-ms-docs-operation-type", "operation") + @get + op listSharedWithMe( + @doc("Expand related entities") + @query(#{ explode: false }) + `$expand`?: "thumbnails"[], + ): SharedItemsRetrieved | OdataError; +} + +@route("/v1.0/me/drive/items/{item-id}/follow") +namespace MeDriveItemFollow { + + @summary("Follow a DriveItem") + @doc("Follow a DriveItem.") + @operationId("FollowDriveItem") + @tag("me.drive") + @extension("x-ms-docs-operation-type", "operation") + @post + op followDriveItem(...ItemIdPath): FollowedDriveItemCreated | OdataError; +} + +@route("/v1.0/me/drive/following/{item-id}") +namespace MeDriveFollowing { + + @summary("Unfollow a DriveItem") + @doc("Unfollow a DriveItem.") + @operationId("UnfollowDriveItem") + @tag("me.drive") + @extension("x-ms-docs-operation-type", "operation") + @delete + op unfollowDriveItem(...ItemIdPath): UnfollowedNoContent | OdataError; +} From dd7aa97acf1bdd4be24881b8b6cfe5bc9def02c7 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Tue, 12 May 2026 22:47:07 +0200 Subject: [PATCH 05/45] feat(spec): add sharing/permissions surface for drives + driveItems Adds the 14 v1beta1 link/invite/permission operations on space roots and driveItems plus the schemas needed by the permission payload: Operations (drives.root tag): - POST /v1beta1/drives/{drive-id}/root/children (CreateDriveItem) - POST /v1beta1/drives/{drive-id}/root/createLink (CreateLinkSpaceRoot) - POST /v1beta1/drives/{drive-id}/root/invite (InviteSpaceRoot) - GET /v1beta1/drives/{drive-id}/root/permissions (ListPermissionsSpaceRoot) - GET/PATCH/DELETE /v1beta1/drives/{drive-id}/root/permissions/{perm-id} (Get/Update/DeletePermissionSpaceRoot) - POST /v1beta1/drives/{drive-id}/root/permissions/{perm-id}/setPassword (SetPermissionPasswordSpaceRoot) Operations (drives.permissions tag): - POST /v1beta1/drives/{drive-id}/items/{item-id}/createLink (CreateLink) - POST /v1beta1/drives/{drive-id}/items/{item-id}/invite (Invite) - GET /v1beta1/drives/{drive-id}/items/{item-id}/permissions (ListPermissions) - GET/PATCH/DELETE /v1beta1/drives/{drive-id}/items/{item-id}/permissions/{perm-id} (Get/Update/DeletePermission) - POST /v1beta1/drives/{drive-id}/items/{item-id}/permissions/{perm-id}/setPassword (SetPermissionPassword) Schemas: sharingLinkType union, sharingLink (full, replacing the stub in models.tsp), driveRecipient, driveItemCreateLink, driveItemInvite, sharingLinkPassword, unifiedRoleDefinition, unifiedRolePermission. Shared parameters: permissionsFilter, permissionsSelect, count, top, plus the perm-id path parameter spread. Request bodies on createLink, invite and createDriveItem are marked optional to match the existing 'requestBody without required: true' shape. The me- and drives-namespaces are now structurally complete; remaining oasdiff errors against /v1.0/me + /v1*/drives are exclusively the known 'type: object + properties' inline-vs-allOf pattern that TypeSpec emits when a subtype redeclares an inherited property as required. --- spec/drives.tsp | 2 +- spec/main.tsp | 1 + spec/models.tsp | 8 +- spec/permissions.tsp | 638 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 643 insertions(+), 6 deletions(-) create mode 100644 spec/permissions.tsp diff --git a/spec/drives.tsp b/spec/drives.tsp index 27cd2b6..6a7a2b4 100644 --- a/spec/drives.tsp +++ b/spec/drives.tsp @@ -353,7 +353,7 @@ model permission { grantedToV2?: sharePointIdentitySet; // description dropped — direct $ref, see userBase.drive note. - link?: sharingLink; + link?: sharingLinkModel; roles?: string[]; diff --git a/spec/main.tsp b/spec/main.tsp index fb34a17..b829e73 100644 --- a/spec/main.tsp +++ b/spec/main.tsp @@ -5,6 +5,7 @@ import "@typespec/openapi3"; import "./models.tsp"; import "./me.tsp"; import "./drives.tsp"; +import "./permissions.tsp"; using Http; using OpenAPI; diff --git a/spec/models.tsp b/spec/models.tsp index 1f48e17..3a8fc5f 100644 --- a/spec/models.tsp +++ b/spec/models.tsp @@ -102,10 +102,10 @@ model appRoleAssignment { appRoleId: string; @format("uuid") - principalId: string; + principalId: string | null; @format("uuid") - resourceId: string; + resourceId: string | null; } model group {} @@ -180,14 +180,12 @@ model userResource extends userBase { // ----------------------------------------------------------------------------- // driveItem, quota and the driveItem facet schemas are defined in `drives.tsp`. -// Stubs for shared schemas defined in other (not-yet-migrated) domains: +// sharingLink and the sharing/permissions schemas are defined in `permissions.tsp`. model sharePointIdentitySet { user?: identity; group?: identity; } -model sharingLink {} - model itemReference { @doc("Unique identifier of the drive instance that contains the item. Read-only.") driveId?: string; diff --git a/spec/permissions.tsp b/spec/permissions.tsp new file mode 100644 index 0000000..a4432fa --- /dev/null +++ b/spec/permissions.tsp @@ -0,0 +1,638 @@ +import "@typespec/http"; +import "@typespec/openapi"; + +import "./models.tsp"; +import "./drives.tsp"; + +using Http; +using OpenAPI; + +namespace LibreGraph; + +// ============================================================================= +// Sharing schemas +// ============================================================================= + +@doc(""" + The type of the link created. + + | Value | Display name | Description | + | -------------- | ----------------- | --------------------------------------------------------------- | + | internal | Internal | Creates an internal link without any permissions. | + | view | View | Creates a read-only link to the driveItem. | + | upload | Upload | Creates a read-write link to the folder driveItem. | + | edit | Edit | Creates a read-write link to the driveItem. | + | createOnly | File Drop | Creates an upload-only link to the folder driveItem. | + | blocksDownload | Secure View | Creates a read-only link that blocks download to the driveItem. | + """) +union sharingLinkType { + internal: "internal", + view: "view", + upload: "upload", + edit: "edit", + createOnly: "createOnly", + blocksDownload: "blocksDownload", +} + +@friendlyName("sharingLink") +@doc(""" + The `SharingLink` resource groups link-related data items into a single structure. + + If a `permission` resource has a non-null `sharingLink` facet, the permission represents a sharing link (as opposed to permissions granted to a person or group). + """) +model sharingLinkModel { + type?: sharingLinkType; + + @doc("If `true` then the user can only use this link to view the item on the web, and cannot use it to download the contents of the item.") + preventsDownload?: boolean; + + @doc("A URL that opens the item in the browser on the website.") + webUrl?: string; + + @doc("Provides a user-visible display name of the link. Optional. Libregraph only.") + `@libre.graph.displayName`?: string; + + @doc("The quicklink property can be assigned to only one link per resource. A quicklink can be used in the clients to provide a one-click copy to clipboard action. Optional. Libregraph only.") + `@libre.graph.quickLink`?: boolean; +} + +@doc(""" + Represents a person, group, or other recipient to share a drive item with using the invite action. + + When using invite to add permissions, the `driveRecipient` object would specify the `email`, `alias`, + or `objectId` of the recipient. Only one of these values is required; multiple values are not accepted. + """) +model driveRecipient { + @doc("The unique identifier for the recipient in the directory.") + objectId?: string; + + @doc("When the recipient is referenced by objectId this annotation is used to differentiate `user` and `group` recipients.") + `@libre.graph.recipient.type`?: string = "user"; +} + +model driveItemCreateLink { + type?: sharingLinkType; + + @doc("Optional. A String with format of yyyy-MM-ddTHH:mm:ssZ of DateTime indicates the expiration time of the permission.") + expirationDateTime?: utcDateTime; + + @doc("Optional.The password of the sharing link that is set by the creator.") + password?: string; + + @doc("Provides a user-visible display name of the link. Optional. Libregraph only.") + displayName?: string; + + @doc("The quicklink property can be assigned to only one link per resource. A quicklink can be used in the clients to provide a one-click copy to clipboard action. Optional. Libregraph only.") + `@libre.graph.quickLink`?: boolean; +} + +model driveItemInvite { + @doc("A collection of recipients who will receive access and the sharing invitation. Currently, only internal users or groups are supported.") + recipients?: driveRecipient[]; + + @doc("Specifies the roles that are to be granted to the recipients of the sharing invitation.") + roles?: string[]; + + @doc("Specifies the actions that are to be granted to the recipients of the sharing invitation, in effect creating a custom role.") + `@libre.graph.permissions.actions`?: string[]; + + @doc("Specifies the dateTime after which the permission expires.") + expirationDateTime?: utcDateTime; +} + +@doc("The sharing link password which should be set.") +model sharingLinkPassword { + @doc("Password. It may require a password policy.") + password?: string; +} + +@doc(""" + Represents a collection of allowed resource actions and the conditions that must be met for the action to be allowed. + Resource actions are tasks that can be performed on a resource. For example, an application resource may support + create, update, delete, and reset password actions. + """) +model unifiedRolePermission { + @doc(""" + Set of tasks that can be performed on a resource. Required. + + The following is the schema for resource actions: + + ``` + {Namespace}/{Entity}/{PropertySet}/{Action} + ``` + + For example: `libre.graph/applications/credentials/update` + """) + allowedResourceActions?: string[]; + + @doc(""" + A conditional expression evaluating membership in a role. + + For now we only support a limited set of operators: + * `exists` - true if the right side property exists. Example: `exists @Resource.File`. We use this to assign different roles to files and folders. + """) + condition?: string; +} + +@doc(""" + A role definition is a collection of permissions in libre graph listing the operations that can be performed + and the resources against which they can performed. + """) +model unifiedRoleDefinition { + @doc("The description for the unifiedRoleDefinition.") + description?: string; + + @doc("The display name for the unifiedRoleDefinition. Required. Supports $filter (`eq`, `in`).") + displayName?: string; + + @doc("The unique identifier for the role definition. Key, not nullable, Read-only. Inherited from entity. Supports $filter (`eq`, `in`).") + id?: string; + + @doc("List of permissions included in the role.") + rolePermissions?: unifiedRolePermission[]; + + @doc(""" + When presenting a list of roles the weight can be used to order them in a meaningful way. + Lower weight gets higher precedence. So content with lower weight will come first. If set, + weights should be non-zero, as 0 is interpreted as an unset weight. + """) + `@libre.graph.weight`?: int32; +} + +// ============================================================================= +// Shared parameters +// ============================================================================= + +@friendlyName("permissionsFilter") +model PermissionsFilter { + @query + @doc("Filter items by property values. By default all permissions are returned and the avalable sharing roles are limited to normal users. To get a list of sharing roles applicable to federated users use the example $select query and combine it with $filter to omit the list of permissions.") + `$filter`?: string; +} + +@friendlyName("permissionsSelect") +model PermissionsSelect { + @query(#{ explode: false }) + @doc("Select properties to be returned. By default all properties are returned. Select the roles property to fetch the available sharing roles without resolving all the permissions. Combine this with the $filter parameter to fetch the actions applicable to federated users.") + `$select`?: ("@libre.graph.permissions.actions.allowedValues" | "@libre.graph.permissions.roles.allowedValues" | "value")[]; +} + +@friendlyName("count") +model CountParam { + @query + @doc("Include count of items") + `$count`?: boolean; +} + +@friendlyName("top") +model TopParam { + @query + @doc("Show only the first n items") + @minValue(0) + `$top`?: integer; +} + +model PermIdPath { + @path + @extension("x-ms-docs-key-type", "permission") + @doc("key: id of permission") + `perm-id`: string; +} + +// ============================================================================= +// Collection responses +// ============================================================================= + +@doc("Retrieved resource") +model PermissionsCollectionRetrieved { + @statusCode _: 200; + @body body: permissionsWithAllowedValuesResponse; +} + +model permissionsWithAllowedValuesResponse { + @doc("A list of role definitions that can be chosen for the resource.") + `@libre.graph.permissions.roles.allowedValues`?: unifiedRoleDefinition[]; + + @doc(""" + A list of actions that can be chosen for a custom role. + + Following the CS3 API we can represent the CS3 permissions by mapping them to driveItem properties or relations like this: + | [CS3 ResourcePermission](https://cs3org.github.io/cs3apis/#cs3.storage.provider.v1beta1.ResourcePermissions) | action | comment | + | ------------------------------------------------------------------------------------------------------------ | ------ | ------- | + | `stat` | `libre.graph/driveItem/basic/read` | `basic` because it does not include versions or trashed items | + | `get_quota` | `libre.graph/driveItem/quota/read` | read only the `quota` property | + | `get_path` | `libre.graph/driveItem/path/read` | read only the `path` property | + | `move` | `libre.graph/driveItem/path/update` | allows updating the `path` property of a CS3 resource | + | `delete` | `libre.graph/driveItem/standard/delete` | `standard` because deleting is a common update operation | + | `list_container` | `libre.graph/driveItem/children/read` | | + | `create_container` | `libre.graph/driveItem/children/create` | | + | `initiate_file_download` | `libre.graph/driveItem/content/read` | `content` is the property read when initiating a download | + | `initiate_file_upload` | `libre.graph/driveItem/upload/create` | `uploads` are a separate property. postprocessing creates the `content` | + | `add_grant` | `libre.graph/driveItem/permissions/create` | | + | `list_grant` | `libre.graph/driveItem/permissions/read` | | + | `update_grant` | `libre.graph/driveItem/permissions/update` | | + | `remove_grant` | `libre.graph/driveItem/permissions/delete` | | + | `deny_grant` | `libre.graph/driveItem/permissions/deny` | uses a non CRUD action `deny` | + | `list_file_versions` | `libre.graph/driveItem/versions/read` | `versions` is a `driveItemVersion` collection | + | `restore_file_version` | `libre.graph/driveItem/versions/update` | the only `update` action is restore | + | `list_recycle` | `libre.graph/driveItem/deleted/read` | reading a driveItem `deleted` property implies listing | + | `restore_recycle_item` | `libre.graph/driveItem/deleted/update` | the only `update` action is restore | + | `purge_recycle` | `libre.graph/driveItem/deleted/delete` | allows purging deleted `driveItems` | + """) + `@libre.graph.permissions.actions.allowedValues`?: string[]; + + value?: permission[]; + + @doc("The total number of permissions available, only present if the `count` query parameter is set to true.") + `@odata.count`?: int32; +} + +@doc("Response") +model CreateLinkResponse { + @statusCode _: 200; + @body body: permission; +} + +@doc("Partial success response TODO") +model PartialSuccess { + @statusCode _: 207; +} + +@doc("Response") +model InviteResponse { + @statusCode _: 200; + @body body: invitePermissionsResponse; +} + +model invitePermissionsResponse { + value?: permission[]; +} + +@doc("Bad request") +@error +model InviteBadRequest { + @statusCode _: 400; + @body body: OdataError; +} + +@doc("Retrieved resource") +model PermissionRetrieved { + @statusCode _: 200; + @body body: permission; +} + +@doc("Updated permission") +model PermissionUpdated { + @statusCode _: 200; + @body body: permission; +} + +@doc("Response") +model CreateChildItemResponse { + @statusCode _: 200; + @body body: driveItemModel; +} + +// ============================================================================= +// Space root permissions / links / invites +// ============================================================================= + +@route("/v1beta1/drives/{drive-id}/root/children") +namespace DriveRootChildren { + + @summary("Create a drive item") + @doc("You can use the root childrens endpoint to mount a remoteItem in the share jail. The `@client.synchronize` property of the `driveItem` in the [sharedWithMe](#/me.drive/ListSharedWithMe) endpoint will change to true.") + @operationId("CreateDriveItem") + @tag("drives.root") + @extension("x-ms-docs-operation-type", "operation") + @post + op createDriveItem( + ...DriveIdPath, + @doc("In the request body, provide a JSON object with the following parameters. For mounting a share the necessary remoteItem id and permission id can be taken from the [sharedWithMe](#/me.drive/ListSharedWithMe) endpoint.") + @body + body?: driveItemModel, + ): CreateChildItemResponse | OdataError; +} + +@route("/v1beta1/drives/{drive-id}/root/createLink") +namespace DriveRootCreateLink { + + @summary("Create a sharing link for the root item of a Drive") + @doc(""" + You can use the createLink action to share a driveItem via a sharing link. + + The response will be a permission object with the link facet containing the created link details. + + ## Link types + + For now, The following values are allowed for the type parameter. + + | Value | Display name | Description | + | -------------- | ----------------- | --------------------------------------------------------------- | + | view | View | Creates a read-only link to the driveItem. | + | upload | Upload | Creates a read-write link to the folder driveItem. | + | edit | Edit | Creates a read-write link to the driveItem. | + | createOnly | File Drop | Creates an upload-only link to the folder driveItem. | + | blocksDownload | Secure View | Creates a read-only link that blocks download to the driveItem. | + """) + @operationId("CreateLinkSpaceRoot") + @tag("drives.root") + @extension("x-ms-docs-operation-type", "operation") + @post + op createLinkSpaceRoot( + ...DriveIdPath, + @doc("In the request body, provide a JSON object with the following parameters.") + @body + body?: driveItemCreateLink, + ): CreateLinkResponse | PartialSuccess | OdataError; +} + +@route("/v1beta1/drives/{drive-id}/root/invite") +namespace DriveRootInvite { + + @summary("Send a sharing invitation") + @doc(""" + Sends a sharing invitation for the root of a `drive`. A sharing invitation provides permissions to the + recipients and optionally sends them an email with a sharing link. + + The response will be a permission object with the grantedToV2 property containing the created grant details. + + ## Roles property values + For now, roles are only identified by a uuid. There are no hardcoded aliases like `read` or `write` because role actions can be completely customized. + """) + @operationId("InviteSpaceRoot") + @tag("drives.root") + @extension("x-ms-docs-operation-type", "operation") + @post + op inviteSpaceRoot( + ...DriveIdPath, + @doc("In the request body, provide a JSON object with the following parameters. To create a custom role submit a list of actions instead of roles.") + @body + body?: driveItemInvite, + ): InviteResponse | PartialSuccess | InviteBadRequest | OdataError; +} + +@route("/v1beta1/drives/{drive-id}/root/permissions") +namespace DriveRootPermissions { + + @summary("List the effective permissions on the root item of a drive.") + @doc(""" + The permissions collection includes potentially sensitive information and may not be available for every caller. + + * For the owner of the item, all sharing permissions will be returned. This includes co-owners. + * For a non-owner caller, only the sharing permissions that apply to the caller are returned. + * Sharing permission properties that contain secrets (e.g. `webUrl`) are only returned for callers that are able to create the sharing permission. + + All permission objects have an `id`. A permission representing + * a link has the `link` facet filled with details. + * a share has the `roles` property set and the `grantedToV2` property filled with the grant recipient details. + """) + @operationId("ListPermissionsSpaceRoot") + @tag("drives.root") + @extension("x-ms-docs-operation-type", "operation") + @get + op listPermissionsSpaceRoot( + ...DriveIdPath, + ...PermissionsFilter, + ...PermissionsSelect, + ...CountParam, + ...TopParam, + ): PermissionsCollectionRetrieved | OdataError; +} + +@route("/v1beta1/drives/{drive-id}/root/permissions/{perm-id}") +namespace DriveRootPermission { + + @summary("Get a single sharing permission for the root item of a drive") + @doc("Return the effective sharing permission for a particular permission resource.") + @operationId("GetPermissionSpaceRoot") + @tag("drives.root") + @extension("x-ms-docs-operation-type", "operation") + @get + op getPermissionSpaceRoot( + ...DriveIdPath, + ...PermIdPath, + ): PermissionRetrieved | OdataError; + + @summary("Update sharing permission") + @doc(""" + Update the properties of a sharing permission by patching the permission resource. + + Only the `roles`, `expirationDateTime` and `password` properties can be modified this way. + """) + @operationId("UpdatePermissionSpaceRoot") + @tag("drives.root") + @extension("x-ms-docs-operation-type", "operation") + @patch + op updatePermissionSpaceRoot( + ...DriveIdPath, + ...PermIdPath, + @doc("New property values") + @body + body: permission, + ): PermissionUpdated | OdataError; + + @summary("Remove access to a Drive") + @doc(""" + Remove access to the root item of a drive. + + Only sharing permissions that are not inherited can be deleted. The `inheritedFrom` property must be `null`. + """) + @operationId("DeletePermissionSpaceRoot") + @tag("drives.root") + @extension("x-ms-docs-operation-type", "operation") + @delete + op deletePermissionSpaceRoot( + ...DriveIdPath, + ...PermIdPath, + ): NoContentSuccess | OdataError; +} + +@route("/v1beta1/drives/{drive-id}/root/permissions/{perm-id}/setPassword") +namespace DriveRootPermissionSetPassword { + + @summary("Set sharing link password for the root item of a drive") + @doc(""" + Set the password of a sharing permission. + + Only the `password` property can be modified this way. + """) + @operationId("SetPermissionPasswordSpaceRoot") + @tag("drives.root") + @extension("x-ms-docs-operation-type", "operation") + @post + op setPermissionPasswordSpaceRoot( + ...DriveIdPath, + ...PermIdPath, + @doc("New password value") + @body + body: sharingLinkPassword, + ): PermissionUpdated | OdataError; +} + +// ============================================================================= +// DriveItem permissions / links / invites +// ============================================================================= + +@route("/v1beta1/drives/{drive-id}/items/{item-id}/createLink") +namespace DriveItemCreateLink { + + @summary("Create a sharing link for a DriveItem") + @doc(""" + You can use the createLink action to share a driveItem via a sharing link. + + The response will be a permission object with the link facet containing the created link details. + + ## Link types + + For now, The following values are allowed for the type parameter. + + | Value | Display name | Description | + | -------------- | ----------------- | --------------------------------------------------------------- | + | view | View | Creates a read-only link to the driveItem. | + | upload | Upload | Creates a read-write link to the folder driveItem. | + | edit | Edit | Creates a read-write link to the driveItem. | + | createOnly | File Drop | Creates an upload-only link to the folder driveItem. | + | blocksDownload | Secure View | Creates a read-only link that blocks download to the driveItem. | + """) + @operationId("CreateLink") + @tag("drives.permissions") + @extension("x-ms-docs-operation-type", "operation") + @post + op createLink( + ...DriveIdPath, + ...ItemIdPath, + @doc("In the request body, provide a JSON object with the following parameters.") + @body + body?: driveItemCreateLink, + ): CreateLinkResponse | PartialSuccess | OdataError; +} + +@route("/v1beta1/drives/{drive-id}/items/{item-id}/invite") +namespace DriveItemInvite { + + @summary("Send a sharing invitation") + @doc(""" + Sends a sharing invitation for a `driveItem`. A sharing invitation provides permissions to the + recipients and optionally sends them an email with a sharing link. + + The response will be a permission object with the grantedToV2 property containing the created grant details. + + ## Roles property values + For now, roles are only identified by a uuid. There are no hardcoded aliases like `read` or `write` because role actions can be completely customized. + """) + @operationId("Invite") + @tag("drives.permissions") + @extension("x-ms-docs-operation-type", "operation") + @post + op invite( + ...DriveIdPath, + ...ItemIdPath, + @doc("In the request body, provide a JSON object with the following parameters. To create a custom role submit a list of actions instead of roles.") + @body + body?: driveItemInvite, + ): InviteResponse | PartialSuccess | InviteBadRequest | OdataError; +} + +@route("/v1beta1/drives/{drive-id}/items/{item-id}/permissions") +namespace DriveItemPermissions { + + @summary("List the effective sharing permissions on a driveItem.") + @doc(""" + The permissions collection includes potentially sensitive information and may not be available for every caller. + + * For the owner of the item, all sharing permissions will be returned. This includes co-owners. + * For a non-owner caller, only the sharing permissions that apply to the caller are returned. + * Sharing permission properties that contain secrets (e.g. `webUrl`) are only returned for callers that are able to create the sharing permission. + + All permission objects have an `id`. A permission representing + * a link has the `link` facet filled with details. + * a share has the `roles` property set and the `grantedToV2` property filled with the grant recipient details. + """) + @operationId("ListPermissions") + @tag("drives.permissions") + @extension("x-ms-docs-operation-type", "operation") + @get + op listPermissions( + ...DriveIdPath, + ...ItemIdPath, + ...PermissionsFilter, + ...PermissionsSelect, + ...CountParam, + ...TopParam, + ): PermissionsCollectionRetrieved | OdataError; +} + +@route("/v1beta1/drives/{drive-id}/items/{item-id}/permissions/{perm-id}") +namespace DriveItemPermission { + + @summary("Get sharing permission for a file or folder") + @doc("Return the effective sharing permission for a particular permission resource.") + @operationId("GetPermission") + @tag("drives.permissions") + @extension("x-ms-docs-operation-type", "operation") + @get + op getPermission( + ...DriveIdPath, + ...ItemIdPath, + ...PermIdPath, + ): PermissionRetrieved | OdataError; + + @summary("Update sharing permission") + @doc(""" + Update the properties of a sharing permission by patching the permission resource. + + Only the `roles`, `expirationDateTime` and `password` properties can be modified this way. + """) + @operationId("UpdatePermission") + @tag("drives.permissions") + @extension("x-ms-docs-operation-type", "operation") + @patch + op updatePermission( + ...DriveIdPath, + ...ItemIdPath, + ...PermIdPath, + @doc("New property values") + @body + body: permission, + ): PermissionUpdated | OdataError; + + @summary("Remove access to a DriveItem") + @doc(""" + Remove access to a DriveItem. + + Only sharing permissions that are not inherited can be deleted. The `inheritedFrom` property must be `null`. + """) + @operationId("DeletePermission") + @tag("drives.permissions") + @extension("x-ms-docs-operation-type", "operation") + @delete + op deletePermission( + ...DriveIdPath, + ...ItemIdPath, + ...PermIdPath, + ): NoContentSuccess | OdataError; +} + +@route("/v1beta1/drives/{drive-id}/items/{item-id}/permissions/{perm-id}/setPassword") +namespace DriveItemPermissionSetPassword { + + @summary("Set sharing link password") + @doc(""" + Set the password of a sharing permission. + + Only the `password` property can be modified this way. + """) + @operationId("SetPermissionPassword") + @tag("drives.permissions") + @extension("x-ms-docs-operation-type", "operation") + @post + op setPermissionPassword( + ...DriveIdPath, + ...ItemIdPath, + ...PermIdPath, + @doc("New password value") + @body + body: sharingLinkPassword, + ): PermissionUpdated | OdataError; +} From bcfb875df9c8a079250be65c9f53f9b915832004 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Tue, 12 May 2026 22:50:36 +0200 Subject: [PATCH 06/45] feat(spec): cover users, groups, invitations, activities Adds the v1.0 surface for the directory-style domains: users (5 + 2 + 1 + 1 ops): - GET/POST /v1.0/users (List/CreateUser) - GET/PATCH/DELETE /v1.0/users/{user-id} (Get/Update/DeleteUser) - POST /v1.0/users/{user-id}/exportPersonalData (ExportPersonalData) - GET/POST /v1.0/users/{user-id}/appRoleAssignments (user.List/CreateAppRoleAssignments) - DELETE /v1.0/users/{user-id}/appRoleAssignments/{appRoleAssignment-id} (user.DeleteAppRoleAssignments) - GET /v1.0/users/{user-id}/photo/$value (GetUserPhoto) groups (2 + 3 + 1 + 1 + 1 ops): - GET/POST /v1.0/groups (List/CreateGroup) - GET/PATCH/DELETE /v1.0/groups/{group-id} (Get/Update/DeleteGroup) - GET /v1.0/groups/{group-id}/members (ListMembers) - POST /v1.0/groups/{group-id}/members/$ref (AddMember) - DELETE /v1.0/groups/{group-id}/members/{directory-object-id}/$ref (DeleteMember) invitations (3 ops): - GET/POST /v1.0/invitations (CreateInvitation, ListInvitations) - GET /v1.0/invitations/{invitation-id} (GetInvitation) activities (1 op): - GET /v1beta1/extensions/org.libregraph/activities (GetActivities) The group model is fleshed out from a stub to its full shape, with @friendlyName("group") so we can keep the GraphOps base/Update naming convention. invitation, invitedUserMessageInfo, recipient and emailAddress are added to models.tsp. activity (with the nested times and template models) is also added. The 'search' query parameter is factored as a reusable model in groups.tsp since it is shared between groups and users listings. 43 paths now emit; remaining v1.0 surface is education, applications, roleManagement and the org.libregraph tags endpoint. --- spec/activities.tsp | 32 +++++ spec/groups.tsp | 206 +++++++++++++++++++++++++++++ spec/invitations.tsp | 66 ++++++++++ spec/main.tsp | 4 + spec/models.tsp | 104 ++++++++++++++- spec/users.tsp | 305 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 715 insertions(+), 2 deletions(-) create mode 100644 spec/activities.tsp create mode 100644 spec/groups.tsp create mode 100644 spec/invitations.tsp create mode 100644 spec/users.tsp diff --git a/spec/activities.tsp b/spec/activities.tsp new file mode 100644 index 0000000..5d71f33 --- /dev/null +++ b/spec/activities.tsp @@ -0,0 +1,32 @@ +import "@typespec/http"; +import "@typespec/openapi"; + +import "./models.tsp"; + +using Http; +using OpenAPI; + +namespace LibreGraph; + +@doc("Found activities") +model ActivitiesFound { + @statusCode _: 200; + @body body: activitiesResponse; +} + +model activitiesResponse { + value?: activity[]; +} + +@route("/v1beta1/extensions/org.libregraph/activities") +namespace Activities { + + @summary("Get activities") + @operationId("GetActivities") + @tag("activities") + @get + op getActivities( + @query + kql?: string, + ): ActivitiesFound | OdataError; +} diff --git a/spec/groups.tsp b/spec/groups.tsp new file mode 100644 index 0000000..2382674 --- /dev/null +++ b/spec/groups.tsp @@ -0,0 +1,206 @@ +import "@typespec/http"; +import "@typespec/openapi"; + +import "./models.tsp"; + +using Http; +using OpenAPI; + +namespace LibreGraph; + +// ============================================================================= +// Shared query parameters / response shapes +// ============================================================================= + +@friendlyName("search") +model SearchParam { + @query + @doc("Search items by search phrases") + `$search`?: string; +} + +model GroupIdPath { + @path + @extension("x-ms-docs-key-type", "group") + @doc("key: id or name of group") + `group-id`: string; +} + +@doc("Retrieved entities") +model GroupsRetrieved { + @statusCode _: 200; + @body body: groupsResponse; +} + +model groupsResponse { + value?: groupModel[]; + + `@odata.nextLink`?: string; +} + +@doc("Created entity") +model GroupCreated { + @statusCode _: 201; + @body body: groupModel; +} + +@doc("Retrieved entity") +model GroupRetrieved { + @statusCode _: 200; + @body body: groupModel; +} + +@doc("Retrieved group members") +model GroupMembersRetrieved { + @statusCode _: 200; + @body body: groupMembersResponse; +} + +model groupMembersResponse { + value?: userResource[]; +} + +model memberReference { + `@odata.id`?: string; +} + +// ============================================================================= +// Operations +// ============================================================================= + +@route("/v1.0/groups") +namespace Groups { + + @summary("Get entities from groups") + @operationId("ListGroups") + @tag("groups") + @extension( + "x-ms-pageable", + #{ nextLinkName: "@odata.nextLink", operationName: "listMore" } + ) + @extension("x-ms-docs-operation-type", "operation") + @get + op listGroups( + ...SearchParam, + + @query(#{ explode: false }) + @doc("Order items by property values") + `$orderby`?: ("displayName" | "displayName desc")[], + + @query(#{ explode: false }) + @doc("Select properties to be returned") + `$select`?: ("id" | "description" | "displayName" | "mail" | "members")[], + + @query(#{ explode: false }) + @doc("Expand related entities") + `$expand`?: "members"[], + ): GroupsRetrieved | OdataError; + + @summary("Add new entity to groups") + @operationId("CreateGroup") + @tag("groups") + @extension("x-ms-docs-operation-type", "operation") + @post + op createGroup( + @doc("New entity") + @body + body: groupModel, + ): GroupCreated | OdataError; +} + +@route("/v1.0/groups/{group-id}") +namespace Group { + + @summary("Get entity from groups by key") + @operationId("GetGroup") + @tag("group") + @extension("x-ms-docs-operation-type", "operation") + @get + op getGroup( + ...GroupIdPath, + + @query(#{ explode: false }) + @doc("Select properties to be returned") + `$select`?: ("id" | "description" | "displayName" | "members")[], + + @query(#{ explode: false }) + @doc("Expand related entities") + `$expand`?: "members"[], + ): GroupRetrieved | OdataError; + + @summary("Update entity in groups") + @operationId("UpdateGroup") + @tag("group") + @extension("x-ms-docs-operation-type", "operation") + @patch + op updateGroup( + ...GroupIdPath, + @doc("New property values") + @body + body: groupModel, + ): NoContentSuccess | OdataError; + + @summary("Delete entity from groups") + @operationId("DeleteGroup") + @tag("group") + @extension("x-ms-docs-operation-type", "operation") + @delete + op deleteGroup( + ...GroupIdPath, + + @header("If-Match") + @doc("ETag") + ifMatch?: string, + ): NoContentSuccess | OdataError; +} + +@route("/v1.0/groups/{group-id}/members") +namespace GroupMembers { + + @summary("Get a list of the group's direct members") + @operationId("ListMembers") + @tag("group") + @get + op listMembers( + @path + @doc("key: id or name of group") + `group-id`: string, + ): GroupMembersRetrieved | OdataError; +} + +@route("/v1.0/groups/{group-id}/members/$ref") +namespace GroupMembersRef { + + @summary("Add a member to a group") + @operationId("AddMember") + @tag("group") + @extension("x-ms-docs-operation-type", "operation") + @post + op addMember( + ...GroupIdPath, + @doc("Object to be added as member") + @body + body: memberReference, + ): NoContentSuccess | OdataError; +} + +@route("/v1.0/groups/{group-id}/members/{directory-object-id}/$ref") +namespace GroupMemberRef { + + @summary("Delete member from a group") + @operationId("DeleteMember") + @tag("group") + @extension("x-ms-docs-operation-type", "operation") + @delete + op deleteMember( + ...GroupIdPath, + + @path + @doc("key: id of group member to remove") + `directory-object-id`: string, + + @header("If-Match") + @doc("ETag") + ifMatch?: string, + ): NoContentSuccess | OdataError; +} diff --git a/spec/invitations.tsp b/spec/invitations.tsp new file mode 100644 index 0000000..8c16b5d --- /dev/null +++ b/spec/invitations.tsp @@ -0,0 +1,66 @@ +import "@typespec/http"; +import "@typespec/openapi"; + +import "./models.tsp"; + +using Http; +using OpenAPI; + +namespace LibreGraph; + +@doc("Invitation created successfully") +model InvitationCreated { + @statusCode _: 201; + @body body: invitation; +} + +@doc("Retrieved invitations") +model InvitationsRetrieved { + @statusCode _: 200; + @body body: invitationsResponse; +} + +@doc("Retrieved invitation") +model InvitationRetrieved { + @statusCode _: 200; + @body body: invitation; +} + +model invitationsResponse { + value?: invitation[]; +} + +@route("/v1.0/invitations") +namespace Invitations { + + @summary("Create a new invitation") + @operationId("CreateInvitation") + @tag("invitations") + @extension("x-ms-docs-operation-type", "operation") + @post + op createInvitation( + @doc("New invitation") + @body + body?: invitation, + ): InvitationCreated; + + @summary("Get a list of invitations") + @operationId("ListInvitations") + @tag("invitations") + @get + op listInvitations(): InvitationsRetrieved; +} + +@route("/v1.0/invitations/{invitation-id}") +namespace Invitation { + + @summary("Get an invitation by key") + @operationId("GetInvitation") + @tag("invitations") + @get + op getInvitation( + @path + @doc("key: id of invitation") + `invitation-id`: string, + ): InvitationRetrieved; +} diff --git a/spec/main.tsp b/spec/main.tsp index b829e73..b500d24 100644 --- a/spec/main.tsp +++ b/spec/main.tsp @@ -6,6 +6,10 @@ import "./models.tsp"; import "./me.tsp"; import "./drives.tsp"; import "./permissions.tsp"; +import "./users.tsp"; +import "./groups.tsp"; +import "./invitations.tsp"; +import "./activities.tsp"; using Http; using OpenAPI; diff --git a/spec/models.tsp b/spec/models.tsp index 3a8fc5f..4593116 100644 --- a/spec/models.tsp +++ b/spec/models.tsp @@ -108,7 +108,50 @@ model appRoleAssignment { resourceId: string | null; } -model group {} +@friendlyName("group") +model groupModel { + @doc("Read-only.") + id?: string; + + @doc("An optional description for the group. Returned by default.") + description?: string; + + @doc("The display name for the group. This property is required when a group is created and cannot be cleared during updates. Returned by default. Supports $search and $orderBy.") + displayName?: string; + + @doc("Specifies the group types. In MS Graph a group can have multiple types, so this is an array. In libreGraph the possible group types deviate from the MS Graph. The only group type that we currently support is \"ReadOnly\", which is set for groups that cannot be modified on the current instance.") + groupTypes?: string[]; + + @doc("Users and groups that are members of this group. HTTP Methods: GET (supported for all groups), Nullable. Supports $expand.") + members?: userResource[]; + + @minItems(1) + @maxItems(20) + @doc("A list of member references to the members to be added. Up to 20 members can be added with a single request") + `members@odata.bind`?: string[]; +} + +@doc("Represents activity.") +model activity { + @doc("Activity ID.") + id: string; + + times: activityTimes; + template: activityTemplate; +} + +model activityTimes { + @doc("Timestamp of the activity.") + recordedTime: utcDateTime; +} + +model activityTemplate { + @doc("Activity description.") + message: string; + + @doc("Activity description variables.") + variables?: {}; +} @friendlyName("userUpdate") @doc("Represents updates to an Active Directory user object.") @@ -144,7 +187,7 @@ model userBase { mail?: string; @doc("Groups that this user is a member of. HTTP Methods: GET (supported for all groups). Read-only. Nullable. Supports $expand.") - memberOf?: group[]; + memberOf?: groupModel[]; @doc("Contains the on-premises SAM account name synchronized from the on-premises directory.") onPremisesSamAccountName?: string; @@ -186,6 +229,63 @@ model sharePointIdentitySet { group?: identity; } +@doc("Represents an email address.") +model emailAddress { + @doc("The email address.") + address?: string; + + @doc("The name associated with the email address.") + name?: string; +} + +@doc("Represents a recipient of an invitation.") +model recipient { + // description dropped — direct $ref, see userBase.drive note. + emailAddress?: emailAddress; +} + +@doc("Additional information about the invitation message.") +model invitedUserMessageInfo { + @doc("Additional recipients who will receive a copy of the invitation message.") + ccRecipients?: recipient[]; + + @doc("The customized message body that will be included in the invitation message.") + customizedMessageBody?: string; + + @doc("The language of the invitation message.") + messageLanguage?: string; +} + +@doc("Represents an invitation to a drive item.") +model invitation { + @doc("The display name of the user being invited.") + invitedUserDisplayName?: string; + + @doc("The email address of the user being invited. Required.") + invitedUserEmailAddress?: string; + + // description dropped — direct $ref, see userBase.drive note. + invitedUserMessageInfo?: invitedUserMessageInfo; + + @doc("Indicates whether an invitation message should be sent to the user.") + sendInvitationMessage?: boolean; + + @doc("The URL to which the user is redirected after accepting the invitation. Required.") + inviteRedirectUrl?: string; + + @doc("The URL that the user can use to redeem the invitation. Read-only.") + inviteRedeemUrl?: string; + + @doc("The status of the invitation. Read-only.") + status?: string; + + // description dropped — direct $ref, see userBase.drive note. + invitedUser?: userResource; + + @doc("The type of user being invited.") + invitedUserType?: string; +} + model itemReference { @doc("Unique identifier of the drive instance that contains the item. Read-only.") driveId?: string; diff --git a/spec/users.tsp b/spec/users.tsp new file mode 100644 index 0000000..3a74e45 --- /dev/null +++ b/spec/users.tsp @@ -0,0 +1,305 @@ +import "@typespec/http"; +import "@typespec/openapi"; + +import "./models.tsp"; +import "./groups.tsp"; + +using Http; +using OpenAPI; + +namespace LibreGraph; + +// ============================================================================= +// Shared parameters / response shapes +// ============================================================================= + +@friendlyName("usersFilter") +model UsersFilter { + @query + @doc("Filter users by property values and relationship attributes") + `$filter`?: string; +} + +model UserIdPath { + @path + @extension("x-ms-docs-key-type", "user") + @doc("key: id or name of user") + `user-id`: string; +} + +@doc("Retrieved entities") +model UsersRetrieved { + @statusCode _: 200; + @body body: usersResponse; +} + +model usersResponse { + value?: userResource[]; + + `@odata.nextLink`?: string; +} + +@doc("Created entity") +model UserCreated { + @statusCode _: 201; + @body body: userResource; +} + +@doc("Retrieved entity") +model UserRetrieved2 { + @statusCode _: 200; + @body body: userResource; +} + +@doc("Retrieved appRoleAssignments") +model AppRoleAssignmentsRetrieved { + @statusCode _: 200; + @body body: appRoleAssignmentsResponse; +} + +model appRoleAssignmentsResponse { + @maxItems(100) + value?: appRoleAssignment[]; + + `@odata.nextLink`?: string; +} + +@doc("Created new app role assignment.") +model AppRoleAssignmentCreated { + @statusCode _: 200; + @body body: appRoleAssignment; +} + +@doc("Retrieved photo") +model UserPhotoRetrieved { + @statusCode _: 200; + @header contentType: "image/jpeg"; + @body body: bytes; +} + +model exportPersonalDataBody { + @doc("the path where the file should be created in the users personal space") + storageLocation?: string; +} + +@doc("success") +model ExportPersonalDataAccepted { + @statusCode _: 202; +} + +// ============================================================================= +// /v1.0/users +// ============================================================================= + +@route("/v1.0/users") +namespace Users { + + @summary("Get entities from users") + @operationId("ListUsers") + @tag("users") + @extension( + "x-ms-pageable", + #{ nextLinkName: "@odata.nextLink", operationName: "listMore" } + ) + @extension("x-ms-docs-operation-type", "operation") + @get + op listUsers( + ...SearchParam, + ...UsersFilter, + + @query(#{ explode: false }) + @doc("Order items by property values") + `$orderby`?: ( + | "displayName" + | "displayName desc" + | "mail" + | "mail desc" + | "onPremisesSamAccountName" + | "onPremisesSamAccountName desc" + )[], + + @query(#{ explode: false }) + @doc("Select properties to be returned") + `$select`?: ( + | "id" + | "displayName" + | "mail" + | "memberOf" + | "onPremisesSamAccountName" + | "surname" + )[], + + @query(#{ explode: false }) + @doc("Expand related entities") + `$expand`?: ("drive" | "drives" | "memberOf" | "appRoleAssignments")[], + ): UsersRetrieved | OdataError; + + @summary("Add new entity to users") + @operationId("CreateUser") + @tag("users") + @extension("x-ms-docs-operation-type", "operation") + @post + op createUser( + @doc("New entity") + @body + body: userResource, + ): UserCreated | OdataError; +} + +// ============================================================================= +// /v1.0/users/{user-id} +// ============================================================================= + +@route("/v1.0/users/{user-id}") +namespace User { + + @summary("Get entity from users by key") + @operationId("GetUser") + @tag("user") + @extension("x-ms-docs-operation-type", "operation") + @get + op getUser( + ...UserIdPath, + + @query(#{ explode: false }) + @doc("Select properties to be returned") + `$select`?: ( + | "id" + | "displayName" + | "drive" + | "drives" + | "mail" + | "memberOf" + | "onPremisesSamAccountName" + | "surname" + )[], + + @query(#{ explode: false }) + @doc("Expand related entities") + `$expand`?: ("drive" | "drives" | "memberOf" | "appRoleAssignments")[], + ): UserRetrieved2 | OdataError; + + @summary("Update entity in users") + @operationId("UpdateUser") + @tag("user") + @extension("x-ms-docs-operation-type", "operation") + @patch + op updateUser( + ...UserIdPath, + @doc("New property values") + @body + body: userBase, + ): UserRetrieved2 | OdataError; + + @summary("Delete entity from users") + @operationId("DeleteUser") + @tag("user") + @extension("x-ms-docs-operation-type", "operation") + @delete + op deleteUser( + ...UserIdPath, + + @header("If-Match") + @doc("ETag") + ifMatch?: string, + ): NoContentSuccess | OdataError; +} + +// ============================================================================= +// /v1.0/users/{user-id}/exportPersonalData +// ============================================================================= + +@route("/v1.0/users/{user-id}/exportPersonalData") +namespace UserExportPersonalData { + + @summary("export personal data of a user") + @operationId("ExportPersonalData") + @tag("user") + @extension("x-ms-docs-operation-type", "operation") + @post + op exportPersonalData( + ...UserIdPath, + @doc("destination the file should be created at") + @body + body?: exportPersonalDataBody, + ): ExportPersonalDataAccepted | OdataError; +} + +// ============================================================================= +// /v1.0/users/{user-id}/appRoleAssignments +// ============================================================================= + +@route("/v1.0/users/{user-id}/appRoleAssignments") +namespace UserAppRoleAssignments { + + @summary("Get appRoleAssignments from a user") + @doc("Represents the global roles a user has been granted for an application.") + @operationId("user.ListAppRoleAssignments") + @tag("user.appRoleAssignment") + @get + op listAppRoleAssignments( + @path + @doc("key: id of user") + `user-id`: string, + ): AppRoleAssignmentsRetrieved | OdataError; + + @summary("Grant an appRoleAssignment to a user") + @doc(""" + Use this API to assign a global role to a user. To grant an app role assignment to a user, you need three identifiers: + * `principalId`: The `id` of the user to whom you are assigning the app role. + * `resourceId`: The `id` of the resource `servicePrincipal` or `application` that has defined the app role. + * `appRoleId`: The `id` of the `appRole` (defined on the resource service principal or application) to assign to the user. + """) + @operationId("user.CreateAppRoleAssignments") + @tag("user.appRoleAssignment") + @post + op createAppRoleAssignments( + @path + @doc("key: id of user") + `user-id`: string, + + @doc("New app role assignment value") + @body + body: appRoleAssignment, + ): AppRoleAssignmentCreated | OdataError; +} + +// ============================================================================= +// /v1.0/users/{user-id}/appRoleAssignments/{appRoleAssignment-id} +// ============================================================================= + +@route("/v1.0/users/{user-id}/appRoleAssignments/{appRoleAssignment-id}") +namespace UserAppRoleAssignment { + + @summary("Delete the appRoleAssignment from a user") + @operationId("user.DeleteAppRoleAssignments") + @tag("user.appRoleAssignment") + @delete + op deleteAppRoleAssignments( + @path + @doc("key: id of user") + `user-id`: string, + + @path + @doc("key: id of appRoleAssignment. This is the concatenated {user-id}:{appRole-id} separated by a colon.") + `appRoleAssignment-id`: string, + + @header("If-Match") + @doc("ETag") + ifMatch?: string, + ): NoContentSuccess | OdataError; +} + +// ============================================================================= +// /v1.0/users/{user-id}/photo/$value +// ============================================================================= + +@route("/v1.0/users/{user-id}/photo/$value") +namespace UserPhoto { + + @summary("Get the photo of a user") + @operationId("GetUserPhoto") + @tag("user.photo") + @get + op getUserPhoto(...UserIdPath): UserPhotoRetrieved | OdataError; +} From fa4af6ad4e3219786f6d6d7611ad6dc05d97e334 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Tue, 12 May 2026 22:54:52 +0200 Subject: [PATCH 07/45] feat(spec): cover education, applications, roleManagement, tags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Brings the emitted path count to 66 — full parity with the existing v1.0.yaml. education (27 ops): - educationUser CRUD + List/Get/Patch/Delete on `/v1.0/education/users` (ListEducationUsers, CreateEducationUser, GetEducationUser, UpdateEducationUser, DeleteEducationUser) - educationSchool CRUD + user/class assignment under `/v1.0/education/schools` (ListSchools, CreateSchool, GetSchool, UpdateSchool, DeleteSchool, ListSchoolUsers, AddUserToSchool, DeleteUserFromSchool, ListSchoolClasses, AddClassToSchool, DeleteClassFromSchool) - educationClass CRUD + member/teacher assignment under `/v1.0/education/classes` (ListClasses, CreateClass, GetClass, UpdateClass, DeleteClass, ListClassMembers, AddUserToClass, DeleteUserFromClass, GetTeachers, AddTeacherToClass, DeleteTeacherFromClass) applications (2 ops): - GET /v1.0/applications (ListApplications) - GET /v1.0/applications/{application-id} (GetApplication) roleManagement (2 ops): - GET /v1beta1/roleManagement/permissions/roleDefinitions (ListPermissionRoleDefinitions) - GET /v1beta1/roleManagement/permissions/roleDefinitions/{role-id} (GetPermissionRoleDefinition) tags (3 ops): - GET/PUT/DELETE /v1.0/extensions/org.libregraph/tags (GetTags, AssignTags, UnassignTags) Schemas added to models.tsp: educationUser, educationSchool, educationClass, educationClassClassification (union), application, appRole, tagAssignment, tagUnassignment. The per-operation `security: bearerAuth` markings from the education surface are not yet wired (TypeSpec needs the auth scheme declared at service level and applied via @useAuth which requires a real auth model, not a stub). This is informational and does not affect breaking compatibility. --- spec/applications.tsp | 49 ++++ spec/education.tsp | 555 ++++++++++++++++++++++++++++++++++++++++ spec/main.tsp | 4 + spec/models.tsp | 147 +++++++++++ spec/roleManagement.tsp | 49 ++++ spec/tags.tsp | 52 ++++ 6 files changed, 856 insertions(+) create mode 100644 spec/applications.tsp create mode 100644 spec/education.tsp create mode 100644 spec/roleManagement.tsp create mode 100644 spec/tags.tsp diff --git a/spec/applications.tsp b/spec/applications.tsp new file mode 100644 index 0000000..e9a57e9 --- /dev/null +++ b/spec/applications.tsp @@ -0,0 +1,49 @@ +import "@typespec/http"; +import "@typespec/openapi"; + +import "./models.tsp"; + +using Http; +using OpenAPI; + +namespace LibreGraph; + +@doc("Retrieved entities") +model ApplicationsRetrieved { + @statusCode _: 200; + @body body: applicationsResponse; +} + +model applicationsResponse { + value?: application[]; +} + +@doc("OK") +model ApplicationRetrieved { + @statusCode _: 200; + @body body: application; +} + +@route("/v1.0/applications") +namespace Applications { + + @summary("Get all applications") + @operationId("ListApplications") + @tag("applications") + @get + op listApplications(): ApplicationsRetrieved | OdataError; +} + +@route("/v1.0/applications/{application-id}") +namespace Application { + + @summary("Get application by id") + @operationId("GetApplication") + @tag("applications") + @get + op getApplication( + @path + @doc("key: id of application") + `application-id`: string, + ): ApplicationRetrieved | OdataError; +} diff --git a/spec/education.tsp b/spec/education.tsp new file mode 100644 index 0000000..9c791d2 --- /dev/null +++ b/spec/education.tsp @@ -0,0 +1,555 @@ +import "@typespec/http"; +import "@typespec/openapi"; + +import "./models.tsp"; + +using Http; +using OpenAPI; + +namespace LibreGraph; + +// ============================================================================= +// Shared response shapes / path parameters +// ============================================================================= + +model EducationUserIdPath { + @path + @doc("key: id or username of user") + `user-id`: string; +} + +model SchoolIdPath { + @path + @doc("key: id or schoolNumber of school") + `school-id`: string; +} + +model ClassIdPath { + @path + @doc("key: id or externalId of class") + `class-id`: string; +} + +@doc("Retrieved entities") +model EducationUsersRetrieved { + @statusCode _: 200; + @body body: educationUsersResponse; +} + +model educationUsersResponse { + value?: educationUser[]; +} + +@doc("Created entity") +model EducationUserCreated { + @statusCode _: 201; + @body body: educationUser; +} + +@doc("Retrieved entity") +model EducationUserRetrieved { + @statusCode _: 200; + @body body: educationUser; +} + +@doc("Success") +model EducationUserUpdated { + @statusCode _: 200; + @body body: educationUser; +} + +@doc("Success") +model EducationUserUpdatedNoContent { + @statusCode _: 204; +} + +@doc("User not found") +@error +model UserNotFound { + @statusCode _: 404; +} + +@doc("Retrieved entities") +model SchoolsRetrieved { + @statusCode _: 200; + @body body: schoolsResponse; +} + +model schoolsResponse { + value?: educationSchool[]; +} + +@doc("Created entity") +model SchoolCreated { + @statusCode _: 201; + @body body: educationSchool; +} + +@doc("Retrieved entity") +model SchoolRetrieved { + @statusCode _: 200; + @body body: educationSchool; +} + +@doc("Success") +model SchoolUpdated { + @statusCode _: 200; + @body body: educationSchool; +} + +@doc("Retrieved educationUser") +model SchoolUsersRetrieved { + @statusCode _: 200; + @body body: educationUsersResponse; +} + +@doc("Retrieved classes") +model SchoolClassesRetrieved { + @statusCode _: 200; + @body body: educationClassesResponse; +} + +model educationClassesResponse { + value?: educationClass[]; +} + +model educationUserReference { + `@odata.id`?: string; +} + +model classReference { + `@odata.id`?: string; +} + +model classMemberReference { + `@odata.id`?: string; +} + +model classTeacherReference { + `@odata.id`?: string; +} + +@doc("Retrieved entities") +model ClassesRetrieved { + @statusCode _: 200; + @body body: educationClassesResponse; +} + +@doc("Created entity") +model ClassCreated { + @statusCode _: 201; + @body body: educationClass; +} + +@doc("Retrieved entity") +model ClassRetrieved { + @statusCode _: 200; + @body body: educationClass; +} + +@doc("New property values") +model ClassUpdated { + @statusCode _: 200; + @body body: educationClass; +} + +@doc("Retrieved class members") +model ClassMembersRetrieved { + @statusCode _: 200; + @body body: educationUsersResponse; +} + +@doc("Retrieved class teachers") +model ClassTeachersRetrieved { + @statusCode _: 200; + @body body: educationUsersResponse; +} + + +// ============================================================================= +// /v1.0/education/users +// ============================================================================= + +@route("/v1.0/education/users") +namespace EducationUsers { + + @summary("Get entities from education users") + @doc(""" + Retrieves a collection of education users with optional filtering, ordering, and expansion. + + **Filtering by external ID:** + Use `$filter` to query users by their external identifier, for example: + `$filter=externalId eq 'EX12345'` + """) + @operationId("ListEducationUsers") + @tag("educationUser") + @get + op listEducationUsers( + @query + @doc(""" + Filter items by property values. Supports a subset of OData filter expressions. + + **Supported filters:** + - By external ID: `externalId eq 'ext_12345'` + """) + `$filter`?: string, + + @query(#{ explode: false }) + @doc("Order items by property values") + `$orderby`?: ( + | "displayName" + | "displayName desc" + | "mail" + | "mail desc" + | "onPremisesSamAccountName" + | "onPremisesSamAccountName desc" + )[], + + @query(#{ explode: false }) + @doc("Expand related entities") + `$expand`?: "memberOf"[], + ): EducationUsersRetrieved | OdataError; + + @summary("Add new education user") + @operationId("CreateEducationUser") + @tag("educationUser") + @post + op createEducationUser( + @doc("New entity") + @body + body: educationUser, + ): EducationUserCreated | OdataError; +} + +@route("/v1.0/education/users/{user-id}") +namespace EducationUser { + + @summary("Get properties of educationUser") + @operationId("GetEducationUser") + @tag("educationUser") + @get + op getEducationUser( + ...EducationUserIdPath, + + @query(#{ explode: false }) + @doc("Expand related entities") + `$expand`?: "memberOf"[], + ): EducationUserRetrieved | OdataError; + + @summary("Update properties of educationUser") + @operationId("UpdateEducationUser") + @tag("educationUser") + @patch + op updateEducationUser( + ...EducationUserIdPath, + @doc("New property values") + @body + body: educationUser, + ): EducationUserUpdated | EducationUserUpdatedNoContent | OdataError; + + @summary("Delete educationUser") + @doc(""" + Deletes an education user by their internal ID. + + **To delete by external ID:** + If you only have an external ID, you must first retrieve the user's internal ID: + 1. Call `GET /graph/v1.0/education/users?$filter=externalId eq '{value}'` + 2. Extract the `id` from the response + 3. Use that `id` in this DELETE endpoint + + See the [ListEducationUsers](#/educationUser/ListEducationUsers) operation for query details. + """) + @operationId("DeleteEducationUser") + @tag("educationUser") + @delete + op deleteEducationUser( + @path + @doc(""" + key: internal user id (UUID format) or username of user. + + **Note:** If you only have an external ID, first query the user + with `GET /graph/v1.0/education/users?$filter=externalId eq '{value}'` + to retrieve the internal ID. + """) + `user-id`: string, + ): NoContentSuccess | UserNotFound | OdataError; +} + +// ============================================================================= +// /v1.0/education/schools +// ============================================================================= + +@route("/v1.0/education/schools") +namespace Schools { + + @summary("Get a list of schools and their properties") + @doc(""" + Retrieves a collection of education schools with optional filtering and ordering. + + **Filtering by external ID:** + Use `$filter` to query schools by their external identifier, for example: + `$filter=externalId eq 'EX12345'` + """) + @operationId("ListSchools") + @tag("educationSchool") + @get + op listSchools( + @query + @doc(""" + Filter items by property values. Supports a subset of OData filter expressions. + + **Supported filters:** + - By external ID: `externalId eq 'ext_12345'` + """) + `$filter`?: string, + ): SchoolsRetrieved | OdataError; + + @summary("Add new school") + @operationId("CreateSchool") + @tag("educationSchool") + @post + op createSchool( + @doc("New school") + @body + body: educationSchool, + ): SchoolCreated | OdataError; +} + +@route("/v1.0/education/schools/{school-id}") +namespace School { + + @summary("Get the properties of a specific school") + @operationId("GetSchool") + @tag("educationSchool") + @get + op getSchool(...SchoolIdPath): SchoolRetrieved | OdataError; + + @summary("Update properties of a school") + @operationId("UpdateSchool") + @tag("educationSchool") + @patch + op updateSchool( + ...SchoolIdPath, + @doc("New property values") + @body + body: educationSchool, + ): SchoolUpdated | OdataError; + + @summary("Delete school") + @doc("Deletes a school. A school can only be delete if it has the terminationDate property set. And if that termination Date is in the past.") + @operationId("DeleteSchool") + @tag("educationSchool") + @delete + op deleteSchool(...SchoolIdPath): NoContentSuccess | OdataError; +} + +@route("/v1.0/education/schools/{school-id}/users") +namespace SchoolUsers { + + @summary("Get the educationUser resources associated with an educationSchool") + @operationId("ListSchoolUsers") + @tag("educationSchool") + @get + op listSchoolUsers(...SchoolIdPath): SchoolUsersRetrieved | OdataError; +} + +@route("/v1.0/education/schools/{school-id}/users/$ref") +namespace SchoolUsersRef { + + @summary("Assign a user to a school") + @operationId("AddUserToSchool") + @tag("educationSchool") + @post + op addUserToSchool( + ...SchoolIdPath, + @doc("educationUser to be added as member") + @body + body: educationUserReference, + ): NoContentSuccess | OdataError; +} + +@route("/v1.0/education/schools/{school-id}/users/{user-id}/$ref") +namespace SchoolUserRef { + + @summary("Unassign user from a school") + @operationId("DeleteUserFromSchool") + @tag("educationSchool") + @delete + op deleteUserFromSchool( + ...SchoolIdPath, + @path + @doc("key: id or username of the user to unassign from school") + `user-id`: string, + ): NoContentSuccess | OdataError; +} + +@route("/v1.0/education/schools/{school-id}/classes") +namespace SchoolClasses { + + @summary("Get the educationClass resources owned by an educationSchool") + @operationId("ListSchoolClasses") + @tag("educationSchool") + @get + op listSchoolClasses(...SchoolIdPath): SchoolClassesRetrieved | OdataError; +} + +@route("/v1.0/education/schools/{school-id}/classes/$ref") +namespace SchoolClassesRef { + + @summary("Assign a class to a school") + @operationId("AddClassToSchool") + @tag("educationSchool") + @post + op addClassToSchool( + ...SchoolIdPath, + @doc("educationClass to be added as member") + @body + body: classReference, + ): NoContentSuccess | OdataError; +} + +@route("/v1.0/education/schools/{school-id}/classes/{class-id}/$ref") +namespace SchoolClassRef { + + @summary("Unassign class from a school") + @operationId("DeleteClassFromSchool") + @tag("educationSchool") + @delete + op deleteClassFromSchool( + ...SchoolIdPath, + @path + @doc("key: id or externalId of the class to unassign from school") + `class-id`: string, + ): NoContentSuccess | OdataError; +} + +// ============================================================================= +// /v1.0/education/classes +// ============================================================================= + +@route("/v1.0/education/classes") +namespace Classes { + + @summary("list education classes") + @operationId("ListClasses") + @tag("educationClass") + @get + op listClasses(): ClassesRetrieved | OdataError; + + @summary("Add new education class") + @operationId("CreateClass") + @tag("educationClass") + @post + op createClass( + @doc("New entity") + @body + body: educationClass, + ): ClassCreated | OdataError; +} + +@route("/v1.0/education/classes/{class-id}") +namespace Class { + + @summary("Get class by key") + @operationId("GetClass") + @tag("educationClass") + @get + op getClass(...ClassIdPath): ClassRetrieved | OdataError; + + @summary("Update properties of a education class") + @operationId("UpdateClass") + @tag("educationClass") + @patch + op updateClass( + ...ClassIdPath, + @doc("New property values") + @body + body: educationClass, + ): ClassUpdated | NoContentSuccess | OdataError; + + @summary("Delete education class") + @operationId("DeleteClass") + @tag("educationClass") + @delete + op deleteClass(...ClassIdPath): NoContentSuccess | OdataError; +} + +@route("/v1.0/education/classes/{class-id}/members") +namespace ClassMembers { + + @summary("Get the educationClass resources owned by an educationSchool") + @operationId("ListClassMembers") + @tag("educationClass") + @get + op listClassMembers(...ClassIdPath): ClassMembersRetrieved | OdataError; +} + +@route("/v1.0/education/classes/{class-id}/members/$ref") +namespace ClassMembersRef { + + @summary("Assign a user to a class") + @operationId("AddUserToClass") + @tag("educationClass") + @post + op addUserToClass( + ...ClassIdPath, + @doc("educationUser to be added as member") + @body + body: classMemberReference, + ): NoContentSuccess | OdataError; +} + +@route("/v1.0/education/classes/{class-id}/members/{user-id}/$ref") +namespace ClassMemberRef { + + @summary("Unassign user from a class") + @operationId("DeleteUserFromClass") + @tag("educationClass") + @delete + op deleteUserFromClass( + ...ClassIdPath, + @path + @doc("key: id or username of the user to unassign from class") + `user-id`: string, + ): NoContentSuccess | OdataError; +} + +@route("/v1.0/education/classes/{class-id}/teachers") +namespace ClassTeachers { + + @summary("Get the teachers for a class") + @operationId("GetTeachers") + @tag("educationClass.teachers") + @get + op getTeachers(...ClassIdPath): ClassTeachersRetrieved | OdataError; +} + +@route("/v1.0/education/classes/{class-id}/teachers/$ref") +namespace ClassTeachersRef { + + @summary("Assign a teacher to a class") + @operationId("AddTeacherToClass") + @tag("educationClass.teachers") + @post + op addTeacherToClass( + ...ClassIdPath, + @doc("educationUser to be added as teacher") + @body + body: classTeacherReference, + ): NoContentSuccess | OdataError; +} + +@route("/v1.0/education/classes/{class-id}/teachers/{user-id}/$ref") +namespace ClassTeacherRef { + + @summary("Unassign user as teacher of a class") + @operationId("DeleteTeacherFromClass") + @tag("educationClass.teachers") + @delete + op deleteTeacherFromClass( + ...ClassIdPath, + @path + @doc("key: id or username of the user to unassign as teacher") + `user-id`: string, + ): NoContentSuccess | OdataError; +} diff --git a/spec/main.tsp b/spec/main.tsp index b500d24..ee07047 100644 --- a/spec/main.tsp +++ b/spec/main.tsp @@ -10,6 +10,10 @@ import "./users.tsp"; import "./groups.tsp"; import "./invitations.tsp"; import "./activities.tsp"; +import "./education.tsp"; +import "./applications.tsp"; +import "./roleManagement.tsp"; +import "./tags.tsp"; using Http; using OpenAPI; diff --git a/spec/models.tsp b/spec/models.tsp index 4593116..63e9fcf 100644 --- a/spec/models.tsp +++ b/spec/models.tsp @@ -131,6 +131,153 @@ model groupModel { `members@odata.bind`?: string[]; } +// ============================================================================= +// Education domain models +// ============================================================================= + +@doc("Represents a school") +model educationSchool { + @doc("The unique identifier for an entity. Read-only.") + id?: string; + + @doc("The organization name") + displayName?: string; + + @doc("School number") + schoolNumber?: string; + + @doc("External identifier of the school") + externalId?: string; + + @doc("Date and time at which the service for this organization is scheduled to be terminated") + terminationDate?: utcDateTime | null; +} + +union educationClassClassification { + class: "class", + course: "course", +} + +@doc("And extension of group representing a class or course") +model educationClass { + @doc("Read-only.") + id?: string; + + @doc("An optional description for the group. Returned by default.") + description?: string; + + @doc("The display name for the group. This property is required when a group is created and cannot be cleared during updates. Returned by default. Supports $search and $orderBy.") + displayName: string; + + @doc("Users and groups that are members of this group. HTTP Methods: GET (supported for all groups), Nullable. Supports $expand.") + members?: userResource[]; + + @minItems(1) + @maxItems(20) + @doc("A list of member references to the members to be added. Up to 20 members can be added with a single request") + `members@odata.bind`?: string[]; + + @doc("Classification of the group, i.e. \"class\" or \"course\"") + classification: educationClassClassification; + + @doc("An external unique ID for the class") + externalId?: string; +} + +@doc("An extension of user with education-specific attributes") +model educationUser { + @doc("Read-only.") + id?: string; + + @doc("Set to \"true\" when the account is enabled.") + accountEnabled?: boolean; + + @doc("The name displayed in the address book for the user. This value is usually the combination of the user's first name, middle initial, and last name. This property is required when a user is created and it cannot be cleared during updates. Returned by default. Supports $orderby.") + displayName?: string; + + @doc("A collection of drives available for this user. Read-only.") + @maxItems(100) + drives?: driveResource[]; + + @doc("The personal drive of this user. Read-only.") + drive?: driveResource; + + @doc("An external unique ID for the user. Use it to associate a user in another system, such as a student or employee ID number.") + externalId?: string; + + @doc("Identities associated with this account.") + identities?: objectIdentity[]; + + @doc("The SMTP address for the user, for example, 'jeff@contoso.opencloud.com'. Returned by default.") + mail?: string; + + @doc("Groups that this user is a member of. HTTP Methods: GET (supported for all groups). Read-only. Nullable. Supports $expand.") + memberOf?: groupModel[]; + + @doc("Contains the on-premises SAM account name synchronized from the on-premises directory. Read-only.") + onPremisesSamAccountName?: string; + + passwordProfile?: passwordProfile; + + @doc("The user's surname (family name or last name). Returned by default.") + surname?: string; + + @doc("The user's givenName. Returned by default.") + givenName?: string; + + @doc("The user`s default role. Such as \"student\" or \"teacher\"") + primaryRole?: string; + + @doc("The user`s type. This can be either \"Member\" for regular user, \"Guest\" for guest users or \"Federated\" for users imported from a federated instance.") + userType?: string; +} + +// ============================================================================= +// Applications / appRole +// ============================================================================= + +@friendlyName("appRole") +model appRoleModel { + @doc("Specifies whether this app role can be assigned to users and groups (by setting to ['User']), to other application's (by setting to ['Application'], or both (by setting to ['User', 'Application']). App roles supporting assignment to other applications' service principals are also known as application permissions. The 'Application' value is only supported for app roles defined on application entities.") + allowedMemberTypes?: string[]; + + @doc("The description for the app role. This is displayed when the app role is being assigned and, if the app role functions as an application permission, during consent experiences.") + description?: string | null; + + @doc("Display name for the permission that appears in the app role assignment and consent experiences.") + displayName?: string | null; + + @doc("Unique role identifier inside the appRoles collection. When creating a new app role, a new GUID identifier must be provided.") + @pattern("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$") + @format("uuid") + id: string; +} + +model application { + @doc("The unique identifier for the object. 12345678-9abc-def0-1234-56789abcde. The value of the ID property is often, but not exclusively, in the form of a GUID. The value should be treated as an opaque identifier and not based in being a GUID. Null values are not allowed. Read-only.") + id: string; + + @doc("The collection of roles defined for the application. With app role assignments, these roles can be assigned to users, groups, or service principals associated with other applications. Not nullable.") + appRoles?: appRoleModel[]; + + @doc("The display name for the application.") + displayName?: string | null; +} + +// ============================================================================= +// Tag assignment / unassignment +// ============================================================================= + +model tagAssignment { + resourceId: string; + tags: string[]; +} + +model tagUnassignment { + resourceId: string; + tags: string[]; +} + @doc("Represents activity.") model activity { @doc("Activity ID.") diff --git a/spec/roleManagement.tsp b/spec/roleManagement.tsp new file mode 100644 index 0000000..4f94f93 --- /dev/null +++ b/spec/roleManagement.tsp @@ -0,0 +1,49 @@ +import "@typespec/http"; +import "@typespec/openapi"; + +import "./models.tsp"; +import "./permissions.tsp"; + +using Http; +using OpenAPI; + +namespace LibreGraph; + +@doc("A list of permission roles than can be used when sharing with users or groups.") +model RoleDefinitionsListed { + @statusCode _: 200; + @body body: unifiedRoleDefinition[]; +} + +@doc("OK") +model RoleDefinitionRetrieved { + @statusCode _: 200; + @body body: unifiedRoleDefinition; +} + +@route("/v1beta1/roleManagement/permissions/roleDefinitions") +namespace PermissionRoleDefinitions { + + @summary("List roleDefinitions") + @doc("Get a list of `unifiedRoleDefinition` objects for the permissions provider. This list determines the roles that can be selected when creating sharing invites.") + @operationId("ListPermissionRoleDefinitions") + @tag("roleManagement") + @get + op listPermissionRoleDefinitions(): RoleDefinitionsListed | OdataError; +} + +@route("/v1beta1/roleManagement/permissions/roleDefinitions/{role-id}") +namespace PermissionRoleDefinition { + + @summary("Get unifiedRoleDefinition") + @doc("Read the properties and relationships of a `unifiedRoleDefinition` object.") + @operationId("GetPermissionRoleDefinition") + @tag("roleManagement") + @get + op getPermissionRoleDefinition( + @path + @extension("x-ms-docs-key-type", "roleDefinition") + @doc("key: id of roleDefinition") + `role-id`: string, + ): RoleDefinitionRetrieved | OdataError; +} diff --git a/spec/tags.tsp b/spec/tags.tsp new file mode 100644 index 0000000..f425bff --- /dev/null +++ b/spec/tags.tsp @@ -0,0 +1,52 @@ +import "@typespec/http"; +import "@typespec/openapi"; + +import "./models.tsp"; + +using Http; +using OpenAPI; + +namespace LibreGraph; + +@doc("Retrieved tags") +model TagsRetrieved { + @statusCode _: 200; + @body body: tagsResponse; +} + +model tagsResponse { + value?: string[]; +} + +@doc("No content") +model TagsOk { + @statusCode _: 200; +} + +@route("/v1.0/extensions/org.libregraph/tags") +namespace Tags { + + @summary("Get all known tags") + @operationId("GetTags") + @tag("tags") + @get + op getTags(): TagsRetrieved | OdataError; + + @summary("Assign tags to a resource") + @operationId("AssignTags") + @tag("tags") + @put + op assignTags( + @body + body?: tagAssignment, + ): TagsOk | OdataError; + + @summary("Unassign tags from a resource") + @operationId("UnassignTags") + @tag("tags") + @delete + op unassignTags( + @body + body?: tagUnassignment, + ): TagsOk | OdataError; +} From 036fad005ad2883d5a126fb6b39e7dfe9092bf75 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Tue, 12 May 2026 22:58:15 +0200 Subject: [PATCH 08/45] fix(spec): drop descriptions on remaining direct $ref properties - educationClass.classification (was emitting `allOf: [$ref]` wrap which prevents oasdiff from seeing the enum at the property level) - educationUser.drive (same allOf wrap) Clears the last 8 `request-property-all-of-added` and `request-property-enum-value-removed` oasdiff errors. --- spec/models.tsp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/models.tsp b/spec/models.tsp index 63e9fcf..96f712e 100644 --- a/spec/models.tsp +++ b/spec/models.tsp @@ -177,7 +177,7 @@ model educationClass { @doc("A list of member references to the members to be added. Up to 20 members can be added with a single request") `members@odata.bind`?: string[]; - @doc("Classification of the group, i.e. \"class\" or \"course\"") + // description dropped — direct $ref, see userBase.drive note. classification: educationClassClassification; @doc("An external unique ID for the class") @@ -199,7 +199,7 @@ model educationUser { @maxItems(100) drives?: driveResource[]; - @doc("The personal drive of this user. Read-only.") + // description dropped — direct $ref, see userBase.drive note. drive?: driveResource; @doc("An external unique ID for the user. Use it to associate a user in another system, such as a student or employee ID number.") From 5a6e4c78559418a4d74effe6b7298bb05d859fcc Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Wed, 13 May 2026 00:02:18 +0200 Subject: [PATCH 09/45] feat(spec): mark unique-item query params + complete appRoleAssignment - Apply `@TypeSpec.JsonSchema.uniqueItems` to every `$select`, `$expand` and `$orderby` array query parameter that is marked `uniqueItems: true` in the existing v1.0.yaml. The decorator is a no-op against the current @typespec/openapi3 release because `applyIntrinsicDecorators` does not pick up JsonSchema decorators for parameters (filed upstream at microsoft/typespec#10656); it will start emitting once the fix releases. - Flesh out the appRoleAssignment schema (id, deletedDateTime, createdDateTime, principalDisplayName, principalType, resourceDisplayName + the existing required appRoleId/principalId/ resourceId) so the generated Go/TS client structs carry the same properties as those generated from the existing v1.0.yaml. --- spec/drives.tsp | 4 ++++ spec/education.tsp | 5 +++++ spec/groups.tsp | 7 +++++++ spec/me.tsp | 5 +++++ spec/models.tsp | 23 +++++++++++++++++++++++ spec/package-lock.json | 19 +++++++++++++++++++ spec/package.json | 1 + spec/permissions.tsp | 3 +++ spec/users.tsp | 7 +++++++ 9 files changed, 74 insertions(+) diff --git a/spec/drives.tsp b/spec/drives.tsp index 6a7a2b4..1061b33 100644 --- a/spec/drives.tsp +++ b/spec/drives.tsp @@ -1,11 +1,13 @@ import "@typespec/http"; import "@typespec/openapi"; +import "@typespec/json-schema"; import "./models.tsp"; using Http; using OpenAPI; + namespace LibreGraph; // ============================================================================= @@ -546,6 +548,7 @@ model driveItemModel { @friendlyName("drivesSelect") model DrivesSelect { + @TypeSpec.JsonSchema.uniqueItems @query(#{ explode: false }) @doc("Select properties to be returned. By default all properties are returned.") `$select`?: "@libre.graph.hasTrashedItems"[]; @@ -553,6 +556,7 @@ model DrivesSelect { @friendlyName("driveItemSelect") model DriveItemSelect { + @TypeSpec.JsonSchema.uniqueItems @query(#{ explode: false }) @doc("Select additional properties to be returned.") `$select`?: "@microsoft.graph.downloadUrl"[]; diff --git a/spec/education.tsp b/spec/education.tsp index 9c791d2..52fe49b 100644 --- a/spec/education.tsp +++ b/spec/education.tsp @@ -1,11 +1,13 @@ import "@typespec/http"; import "@typespec/openapi"; +import "@typespec/json-schema"; import "./models.tsp"; using Http; using OpenAPI; + namespace LibreGraph; // ============================================================================= @@ -194,6 +196,7 @@ namespace EducationUsers { """) `$filter`?: string, + @TypeSpec.JsonSchema.uniqueItems @query(#{ explode: false }) @doc("Order items by property values") `$orderby`?: ( @@ -205,6 +208,7 @@ namespace EducationUsers { | "onPremisesSamAccountName desc" )[], + @TypeSpec.JsonSchema.uniqueItems @query(#{ explode: false }) @doc("Expand related entities") `$expand`?: "memberOf"[], @@ -231,6 +235,7 @@ namespace EducationUser { op getEducationUser( ...EducationUserIdPath, + @TypeSpec.JsonSchema.uniqueItems @query(#{ explode: false }) @doc("Expand related entities") `$expand`?: "memberOf"[], diff --git a/spec/groups.tsp b/spec/groups.tsp index 2382674..fa18215 100644 --- a/spec/groups.tsp +++ b/spec/groups.tsp @@ -1,11 +1,13 @@ import "@typespec/http"; import "@typespec/openapi"; +import "@typespec/json-schema"; import "./models.tsp"; using Http; using OpenAPI; + namespace LibreGraph; // ============================================================================= @@ -83,14 +85,17 @@ namespace Groups { op listGroups( ...SearchParam, + @TypeSpec.JsonSchema.uniqueItems @query(#{ explode: false }) @doc("Order items by property values") `$orderby`?: ("displayName" | "displayName desc")[], + @TypeSpec.JsonSchema.uniqueItems @query(#{ explode: false }) @doc("Select properties to be returned") `$select`?: ("id" | "description" | "displayName" | "mail" | "members")[], + @TypeSpec.JsonSchema.uniqueItems @query(#{ explode: false }) @doc("Expand related entities") `$expand`?: "members"[], @@ -119,10 +124,12 @@ namespace Group { op getGroup( ...GroupIdPath, + @TypeSpec.JsonSchema.uniqueItems @query(#{ explode: false }) @doc("Select properties to be returned") `$select`?: ("id" | "description" | "displayName" | "members")[], + @TypeSpec.JsonSchema.uniqueItems @query(#{ explode: false }) @doc("Expand related entities") `$expand`?: "members"[], diff --git a/spec/me.tsp b/spec/me.tsp index 4243ca9..0a02672 100644 --- a/spec/me.tsp +++ b/spec/me.tsp @@ -1,11 +1,13 @@ import "@typespec/http"; import "@typespec/openapi"; +import "@typespec/json-schema"; import "./models.tsp"; using Http; using OpenAPI; + namespace LibreGraph; // ----------------------------------------------------------------------------- @@ -92,6 +94,7 @@ namespace Me { @get op getOwnUser( @doc("Expand related entities") + @TypeSpec.JsonSchema.uniqueItems @query(#{ explode: false }) `$expand`?: "memberOf"[], ): UserRetrieved | OdataError; @@ -300,6 +303,7 @@ namespace MeDriveSharedByMe { @get op listSharedByMe( @doc("Expand related entities") + @TypeSpec.JsonSchema.uniqueItems @query(#{ explode: false }) `$expand`?: "thumbnails"[], ): SharedItemsRetrieved | OdataError; @@ -316,6 +320,7 @@ namespace MeDriveSharedWithMe { @get op listSharedWithMe( @doc("Expand related entities") + @TypeSpec.JsonSchema.uniqueItems @query(#{ explode: false }) `$expand`?: "thumbnails"[], ): SharedItemsRetrieved | OdataError; diff --git a/spec/models.tsp b/spec/models.tsp index 96f712e..d08d8b4 100644 --- a/spec/models.tsp +++ b/spec/models.tsp @@ -98,12 +98,35 @@ scalar language extends string; // required-property set is declared so oasdiff comparisons reflect the // existing v1.0.yaml shape. model appRoleAssignment { + @doc("The unique identifier for the object. 12345678-9abc-def0-1234-56789abcde. The value of the ID property is often, but not exclusively, in the form of a GUID. The value should be treated as an opaque identifier and not based in being a GUID. Null values are not allowed. Read-only.") + id?: string; + + deletedDateTime?: utcDateTime; + + @doc("The identifier (id) for the app role which is assigned to the user. Required on create.") + @pattern("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$") @format("uuid") appRoleId: string; + @doc("The time when the app role assignment was created. The Timestamp type represents date and time information using ISO 8601 format and is always in UTC time. For example, midnight UTC on Jan 1, 2014 is 2014-01-01T00:00:00Z. Read-only.") + createdDateTime?: utcDateTime | null; + + @doc("The display name of the user, group, or service principal that was granted the app role assignment. Read-only.") + principalDisplayName?: string | null; + + @doc("The unique identifier (id) for the user, security group, or service principal being granted the app role. Security groups with dynamic memberships are supported. Required on create.") + @pattern("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$") @format("uuid") principalId: string | null; + @doc("The type of the assigned principal. This can either be User, Group, or ServicePrincipal. Read-only.") + principalType?: string | null; + + @doc("The display name of the resource app's service principal to which the assignment is made.") + resourceDisplayName?: string | null; + + @doc("The unique identifier (id) for the resource service principal for which the assignment is made. Required on create.") + @pattern("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$") @format("uuid") resourceId: string | null; } diff --git a/spec/package-lock.json b/spec/package-lock.json index 179085e..612363d 100644 --- a/spec/package-lock.json +++ b/spec/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@typespec/compiler": "^1.5.0", "@typespec/http": "^1.5.0", + "@typespec/json-schema": "^1.12.0", "@typespec/openapi": "^1.5.0", "@typespec/openapi3": "^1.5.0" } @@ -508,6 +509,23 @@ } } }, + "node_modules/@typespec/json-schema": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@typespec/json-schema/-/json-schema-1.12.0.tgz", + "integrity": "sha512-e/hxD6q0ThpCmIXOt4wseC30dv1lWnmQJaDhsn6MJySNVpcfTw9xVgj40Oeygc3TC1nO5X0cICtkOqDV/FMFAw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@typespec/asset-emitter": "^0.79.1", + "yaml": "^2.8.3" + }, + "engines": { + "node": ">=22.0.0" + }, + "peerDependencies": { + "@typespec/compiler": "^1.12.0" + } + }, "node_modules/@typespec/openapi": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/@typespec/openapi/-/openapi-1.11.0.tgz", @@ -591,6 +609,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", diff --git a/spec/package.json b/spec/package.json index 9008b9b..b78a1f9 100644 --- a/spec/package.json +++ b/spec/package.json @@ -9,6 +9,7 @@ "dependencies": { "@typespec/compiler": "^1.5.0", "@typespec/http": "^1.5.0", + "@typespec/json-schema": "^1.12.0", "@typespec/openapi": "^1.5.0", "@typespec/openapi3": "^1.5.0" } diff --git a/spec/permissions.tsp b/spec/permissions.tsp index a4432fa..be271e0 100644 --- a/spec/permissions.tsp +++ b/spec/permissions.tsp @@ -1,5 +1,6 @@ import "@typespec/http"; import "@typespec/openapi"; +import "@typespec/json-schema"; import "./models.tsp"; import "./drives.tsp"; @@ -7,6 +8,7 @@ import "./drives.tsp"; using Http; using OpenAPI; + namespace LibreGraph; // ============================================================================= @@ -172,6 +174,7 @@ model PermissionsFilter { @friendlyName("permissionsSelect") model PermissionsSelect { + @TypeSpec.JsonSchema.uniqueItems @query(#{ explode: false }) @doc("Select properties to be returned. By default all properties are returned. Select the roles property to fetch the available sharing roles without resolving all the permissions. Combine this with the $filter parameter to fetch the actions applicable to federated users.") `$select`?: ("@libre.graph.permissions.actions.allowedValues" | "@libre.graph.permissions.roles.allowedValues" | "value")[]; diff --git a/spec/users.tsp b/spec/users.tsp index 3a74e45..c8358e7 100644 --- a/spec/users.tsp +++ b/spec/users.tsp @@ -1,5 +1,6 @@ import "@typespec/http"; import "@typespec/openapi"; +import "@typespec/json-schema"; import "./models.tsp"; import "./groups.tsp"; @@ -7,6 +8,7 @@ import "./groups.tsp"; using Http; using OpenAPI; + namespace LibreGraph; // ============================================================================= @@ -107,6 +109,7 @@ namespace Users { ...SearchParam, ...UsersFilter, + @TypeSpec.JsonSchema.uniqueItems @query(#{ explode: false }) @doc("Order items by property values") `$orderby`?: ( @@ -118,6 +121,7 @@ namespace Users { | "onPremisesSamAccountName desc" )[], + @TypeSpec.JsonSchema.uniqueItems @query(#{ explode: false }) @doc("Select properties to be returned") `$select`?: ( @@ -129,6 +133,7 @@ namespace Users { | "surname" )[], + @TypeSpec.JsonSchema.uniqueItems @query(#{ explode: false }) @doc("Expand related entities") `$expand`?: ("drive" | "drives" | "memberOf" | "appRoleAssignments")[], @@ -161,6 +166,7 @@ namespace User { op getUser( ...UserIdPath, + @TypeSpec.JsonSchema.uniqueItems @query(#{ explode: false }) @doc("Select properties to be returned") `$select`?: ( @@ -174,6 +180,7 @@ namespace User { | "surname" )[], + @TypeSpec.JsonSchema.uniqueItems @query(#{ explode: false }) @doc("Expand related entities") `$expand`?: ("drive" | "drives" | "memberOf" | "appRoleAssignments")[], From c278869ff98bcb8a78e71d5a6e7c8bffa125da94 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Wed, 13 May 2026 00:28:28 +0200 Subject: [PATCH 10/45] feat(spec): match CollectionOfX schema naming + ExportPersonalDataRequest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The existing v1.0.yaml uses inline collection schemas titled "Collection of X". openapi-generator-cli synthesizes the Go/TS type names from those titles (`CollectionOfActivities`, `CollectionOfDrives`, `CollectionOfDrives1`, etc.). The emitted TypeSpec output used the TypeSpec model identifiers (`activitiesResponse` …) directly as component keys, which produced new names like `ActivitiesResponse` / `DrivesResponse` and broke downstream callers that import the existing type names. Pin the emitted component key via `@friendlyName("CollectionOf…")` on every response-wrapper model so the generated client types match the existing names byte-for-byte: - CollectionOf{Activities, Applications, AppRoleAssignments, Class, EducationClass, EducationUser, Group, Invitations, Permissions, PermissionsWithAllowedValues, Schools, Tags, User, Users, Drives, Drives1, DriveItems, DriveItems1} - {Class, ClassMember, ClassTeacher, EducationUser, Member}Reference Split the previously-single `drivesResponse` model so the four "Collection of drives" call sites land in the right name bucket: ListMyDrives / ListMyDrivesBeta keep `CollectionOfDrives` (paginated, with `maxItems: 100` and `@odata.nextLink`); ListAllDrives / ListAllDrivesBeta get the new `CollectionOfDrives1` (un-paginated, matching the existing shape). `educationClassesResponse` is likewise split into `CollectionOfEducationClass` (school/classes) and a new `classesResponse` for `CollectionOfClass` (/education/classes). Rename the inline exportPersonalData request body schema to `exportPersonalDataRequest` so the generated Go/TS parameter name matches the existing client. After this commit the generated Go client has zero method-signature diffs against the existing libre-graph-api-go output; remaining file diffs are cosmetic (blank-line whitespace and the inline-properties-alongside-allOf pattern that the @typespec/openapi3 emitter produces for required-only overrides). --- spec/activities.tsp | 1 + spec/applications.tsp | 1 + spec/drives.tsp | 7 ++++++- spec/education.tsp | 14 +++++++++++++- spec/groups.tsp | 3 +++ spec/invitations.tsp | 1 + spec/me.tsp | 3 +++ spec/permissions.tsp | 2 ++ spec/tags.tsp | 1 + spec/users.tsp | 6 ++++-- 10 files changed, 35 insertions(+), 4 deletions(-) diff --git a/spec/activities.tsp b/spec/activities.tsp index 5d71f33..0aa013f 100644 --- a/spec/activities.tsp +++ b/spec/activities.tsp @@ -14,6 +14,7 @@ model ActivitiesFound { @body body: activitiesResponse; } +@friendlyName("CollectionOfActivities") model activitiesResponse { value?: activity[]; } diff --git a/spec/applications.tsp b/spec/applications.tsp index e9a57e9..a64364b 100644 --- a/spec/applications.tsp +++ b/spec/applications.tsp @@ -14,6 +14,7 @@ model ApplicationsRetrieved { @body body: applicationsResponse; } +@friendlyName("CollectionOfApplications") model applicationsResponse { value?: application[]; } diff --git a/spec/drives.tsp b/spec/drives.tsp index 1061b33..85a55b7 100644 --- a/spec/drives.tsp +++ b/spec/drives.tsp @@ -590,7 +590,12 @@ model DriveIdPath { @doc("Retrieved spaces") model DrivesListResponse { @statusCode _: 200; - @body body: drivesResponse; + @body body: allDrivesCollection; +} + +@friendlyName("CollectionOfDrives1") +model allDrivesCollection { + value?: driveResource[]; } @doc("Retrieved drive") diff --git a/spec/education.tsp b/spec/education.tsp index 52fe49b..7c6d6e7 100644 --- a/spec/education.tsp +++ b/spec/education.tsp @@ -38,6 +38,7 @@ model EducationUsersRetrieved { @body body: educationUsersResponse; } +@friendlyName("CollectionOfEducationUser") model educationUsersResponse { value?: educationUser[]; } @@ -77,6 +78,7 @@ model SchoolsRetrieved { @body body: schoolsResponse; } +@friendlyName("CollectionOfSchools") model schoolsResponse { value?: educationSchool[]; } @@ -111,22 +113,27 @@ model SchoolClassesRetrieved { @body body: educationClassesResponse; } +@friendlyName("CollectionOfEducationClass") model educationClassesResponse { value?: educationClass[]; } +@friendlyName("EducationUserReference") model educationUserReference { `@odata.id`?: string; } +@friendlyName("ClassReference") model classReference { `@odata.id`?: string; } +@friendlyName("ClassMemberReference") model classMemberReference { `@odata.id`?: string; } +@friendlyName("ClassTeacherReference") model classTeacherReference { `@odata.id`?: string; } @@ -134,7 +141,12 @@ model classTeacherReference { @doc("Retrieved entities") model ClassesRetrieved { @statusCode _: 200; - @body body: educationClassesResponse; + @body body: classesResponse; +} + +@friendlyName("CollectionOfClass") +model classesResponse { + value?: educationClass[]; } @doc("Created entity") diff --git a/spec/groups.tsp b/spec/groups.tsp index fa18215..7a14ebb 100644 --- a/spec/groups.tsp +++ b/spec/groups.tsp @@ -34,6 +34,7 @@ model GroupsRetrieved { @body body: groupsResponse; } +@friendlyName("CollectionOfGroup") model groupsResponse { value?: groupModel[]; @@ -58,10 +59,12 @@ model GroupMembersRetrieved { @body body: groupMembersResponse; } +@friendlyName("CollectionOfUsers") model groupMembersResponse { value?: userResource[]; } +@friendlyName("MemberReference") model memberReference { `@odata.id`?: string; } diff --git a/spec/invitations.tsp b/spec/invitations.tsp index 8c16b5d..ea1dff0 100644 --- a/spec/invitations.tsp +++ b/spec/invitations.tsp @@ -26,6 +26,7 @@ model InvitationRetrieved { @body body: invitation; } +@friendlyName("CollectionOfInvitations") model invitationsResponse { value?: invitation[]; } diff --git a/spec/me.tsp b/spec/me.tsp index 0a02672..1579519 100644 --- a/spec/me.tsp +++ b/spec/me.tsp @@ -164,6 +164,7 @@ namespace MePhoto { // /v1.0/me/drives // ----------------------------------------------------------------------------- +@friendlyName("CollectionOfDrives") model drivesResponse { @maxItems(100) value?: driveResource[]; @@ -248,6 +249,7 @@ model UnfollowedNoContent { @statusCode _: 204; } +@friendlyName("CollectionOfDriveItems") model meDriveChildrenResponse { @maxItems(100) value?: driveItemModel[]; @@ -255,6 +257,7 @@ model meDriveChildrenResponse { `@odata.nextLink`?: string; } +@friendlyName("CollectionOfDriveItems1") model sharedDriveItemsResponse { value?: driveItemModel[]; } diff --git a/spec/permissions.tsp b/spec/permissions.tsp index be271e0..060447d 100644 --- a/spec/permissions.tsp +++ b/spec/permissions.tsp @@ -212,6 +212,7 @@ model PermissionsCollectionRetrieved { @body body: permissionsWithAllowedValuesResponse; } +@friendlyName("CollectionOfPermissionsWithAllowedValues") model permissionsWithAllowedValuesResponse { @doc("A list of role definitions that can be chosen for the resource.") `@libre.graph.permissions.roles.allowedValues`?: unifiedRoleDefinition[]; @@ -267,6 +268,7 @@ model InviteResponse { @body body: invitePermissionsResponse; } +@friendlyName("CollectionOfPermissions") model invitePermissionsResponse { value?: permission[]; } diff --git a/spec/tags.tsp b/spec/tags.tsp index f425bff..5c9654b 100644 --- a/spec/tags.tsp +++ b/spec/tags.tsp @@ -14,6 +14,7 @@ model TagsRetrieved { @body body: tagsResponse; } +@friendlyName("CollectionOfTags") model tagsResponse { value?: string[]; } diff --git a/spec/users.tsp b/spec/users.tsp index c8358e7..379cca1 100644 --- a/spec/users.tsp +++ b/spec/users.tsp @@ -35,6 +35,7 @@ model UsersRetrieved { @body body: usersResponse; } +@friendlyName("CollectionOfUser") model usersResponse { value?: userResource[]; @@ -59,6 +60,7 @@ model AppRoleAssignmentsRetrieved { @body body: appRoleAssignmentsResponse; } +@friendlyName("CollectionOfAppRoleAssignments") model appRoleAssignmentsResponse { @maxItems(100) value?: appRoleAssignment[]; @@ -79,7 +81,7 @@ model UserPhotoRetrieved { @body body: bytes; } -model exportPersonalDataBody { +model exportPersonalDataRequest { @doc("the path where the file should be created in the users personal space") storageLocation?: string; } @@ -228,7 +230,7 @@ namespace UserExportPersonalData { ...UserIdPath, @doc("destination the file should be created at") @body - body?: exportPersonalDataBody, + body?: exportPersonalDataRequest, ): ExportPersonalDataAccepted | OdataError; } From 364b644d444167451be85a3c675fce14f0a7558c Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Wed, 13 May 2026 00:33:29 +0200 Subject: [PATCH 11/45] fix(spec): inline ad-hoc body+enum to avoid extra schema components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Photo PUT/PATCH bodies were sharing a named `PhotoBody` model via spread; that emits the model as a separate component schema, which the typescript-axios generator then exports as an `interface PhotoBody`. The existing typescript-axios client does not have this type. Inline the `@header contentType` + `@body body?` parameters in each photo op so no extra component is created. - The `classification` property on `educationClass` used a named `union educationClassClassification` which the openapi3 emitter extracts into its own component (and therefore an extra exported enum on clients). Inline the enum on the property as `"class" | "course"` so the original behavior — enum values listed inline on the schema property — is preserved. After this commit the typescript-axios client has zero extra exports versus the existing libre-graph-api-typescript-axios client; the only remaining method-signature diffs are the 15 `Set` vs `Array` query parameter types, which depend on the upstream `@uniqueItems` propagation fix at microsoft/typespec#10656. --- spec/me.tsp | 20 ++++++++++---------- spec/models.tsp | 9 ++------- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/spec/me.tsp b/spec/me.tsp index 1579519..60e8dba 100644 --- a/spec/me.tsp +++ b/spec/me.tsp @@ -44,14 +44,6 @@ model DrivesRetrieved { @body body: drivesResponse; } -model PhotoBody { - @header contentType: "image/jpeg"; - - @doc("New user photo") - @body - body?: bytes; -} - // ----------------------------------------------------------------------------- // Shared query parameters (rendered as `#/components/parameters/`) // ----------------------------------------------------------------------------- @@ -142,7 +134,11 @@ namespace MePhoto { @tag("me.photo") @put op updateOwnUserPhotoPut( - ...PhotoBody, + @header contentType: "image/jpeg", + + @doc("New user photo") + @body + body?: bytes, ): NoContentSuccess | OdataError; @summary("Update the current user's profile photo") @@ -150,7 +146,11 @@ namespace MePhoto { @tag("me.photo") @patch op updateOwnUserPhotoPatch( - ...PhotoBody, + @header contentType: "image/jpeg", + + @doc("New user photo") + @body + body?: bytes, ): NoContentSuccess | OdataError; @summary("Delete the current user's profile photo") diff --git a/spec/models.tsp b/spec/models.tsp index d08d8b4..f49e2f9 100644 --- a/spec/models.tsp +++ b/spec/models.tsp @@ -176,11 +176,6 @@ model educationSchool { terminationDate?: utcDateTime | null; } -union educationClassClassification { - class: "class", - course: "course", -} - @doc("And extension of group representing a class or course") model educationClass { @doc("Read-only.") @@ -200,8 +195,8 @@ model educationClass { @doc("A list of member references to the members to be added. Up to 20 members can be added with a single request") `members@odata.bind`?: string[]; - // description dropped — direct $ref, see userBase.drive note. - classification: educationClassClassification; + @doc("Classification of the group, i.e. \"class\" or \"course\"") + classification: "class" | "course"; @doc("An external unique ID for the class") externalId?: string; From 382f5fdaff2832f00c2a8cb151d7cdd981fb1707 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Wed, 13 May 2026 00:59:40 +0200 Subject: [PATCH 12/45] build: regenerate api/openapi-spec/v1.0.yaml from TypeSpec in CI + Makefile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hand-maintained `api/openapi-spec/v1.0.yaml` becomes a build artefact of the TypeSpec sources under `spec/`. Drop it from the repo, add it to `.gitignore` and rebuild it on demand. - `scripts/compile-spec.sh` runs `tsp compile` (with a lazy `npm ci` so the install only happens on first compile). Emits straight to `api/openapi-spec/v1.0.yaml` via the updated `emitter-output-dir` in `spec/tspconfig.yaml`. - `scripts/generate-{go,typescript-axios,php,cpp-qt-client}.sh` carry the per-language `openapi-generator-cli` invocation. They run inside the `openapi-generator-cli` container — same shape as today, just factored out of the YAML so the Makefile can call them too. - Each `.woodpecker/build-*.yaml` pipeline gets a new `compile-spec` step (using `node:22-alpine` for the tsp run) in front of the existing `generate-*` step, which now invokes the matching script instead of an inline command. `docs.yaml` gets the same compile step so rsync to swagger-ui sees the fresh YAML. - A new top-level `Makefile` reads the `openapitools/openapi-generator-cli` image straight out of the matching `.woodpecker/build-.yaml` file (`grep`-based, no `yq` dependency) so renovatebot remains the single source of truth. `make spec` recompiles, `make {go, typescript-axios, php, cpp-qt-client}` materialises each client into `build/clients//` via the same script CI uses. Local verification — `make clean && make spec && make go && make typescript-axios` produces clients byte-identical to the ones generated in the earlier PoC run (zero `diff -rq` against the in-flight comparison set). The 5× compile-spec pipeline overhead (4 `build-*` + `docs`) is deliberately accepted; cross-pipeline artefact passing is not used anywhere in the opencloud Woodpecker setup today. --- .gitignore | 10 +- .woodpecker/build-cpp-qt.yaml | 16 +- .woodpecker/build-go.yaml | 16 +- .woodpecker/build-php.yaml | 16 +- .woodpecker/build-typescript-axios.yaml | 16 +- .woodpecker/docs.yaml | 7 + Makefile | 53 + api/openapi-spec/v1.0.yaml | 6234 ----------------------- scripts/compile-spec.sh | 14 + scripts/generate-cpp-qt-client.sh | 20 + scripts/generate-go.sh | 20 + scripts/generate-php.sh | 20 + scripts/generate-typescript-axios.sh | 19 + spec/tspconfig.yaml | 2 +- 14 files changed, 191 insertions(+), 6272 deletions(-) create mode 100644 Makefile delete mode 100644 api/openapi-spec/v1.0.yaml create mode 100755 scripts/compile-spec.sh create mode 100755 scripts/generate-cpp-qt-client.sh create mode 100755 scripts/generate-go.sh create mode 100755 scripts/generate-php.sh create mode 100755 scripts/generate-typescript-axios.sh diff --git a/.gitignore b/.gitignore index f814c22..71cdd40 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,11 @@ composer.lock vendor -tmp \ No newline at end of file +tmp + +# Generated by `make spec` (`scripts/compile-spec.sh`). +api/openapi-spec/v1.0.yaml + +# Local build artefacts. +build/ +spec/build/ +spec/node_modules/ diff --git a/.woodpecker/build-cpp-qt.yaml b/.woodpecker/build-cpp-qt.yaml index 7e26353..dc936aa 100644 --- a/.woodpecker/build-cpp-qt.yaml +++ b/.woodpecker/build-cpp-qt.yaml @@ -1,6 +1,7 @@ --- variables: - &alpine_image 'owncloudci/alpine:latest' + - &node_image 'node:22-alpine' - &generator_image 'openapitools/openapi-generator-cli:v7.13.0@sha256:52b4b2bb6129b086b0f12614e98c884d378a84349fa114b07e20515702a793c5' - &environment HTTP_PROXY: @@ -21,20 +22,17 @@ steps: ref: HEAD path: libre-graph-api-cpp-qt-client remote: https://github.com/opencloud-eu/libre-graph-api-cpp-qt-client.git + compile-spec: + image: *node_image + environment: *environment + commands: + - sh scripts/compile-spec.sh generate-cpp-qt: image: *generator_image environment: *environment commands: - rm -Rf libre-graph-api-cpp-qt-client/* - - '/usr/local/bin/docker-entrypoint.sh generate - --enable-post-process-file - -i api/openapi-spec/v1.0.yaml - --additional-properties=packageName=libregraph - --git-user-id=opencloud-eu - --git-repo-id=libre-graph-api-cpp-qt-client - -g cpp-qt-client - -t templates/cpp-qt-client - -o libre-graph-api-cpp-qt-client' + - sh scripts/generate-cpp-qt-client.sh - cp LICENSE libre-graph-api-cpp-qt-client/LICENSE diff: image: *alpine_image diff --git a/.woodpecker/build-go.yaml b/.woodpecker/build-go.yaml index 2ce26b8..3ef9ac1 100644 --- a/.woodpecker/build-go.yaml +++ b/.woodpecker/build-go.yaml @@ -1,6 +1,7 @@ --- variables: - &alpine_image 'owncloudci/alpine:latest' + - &node_image 'node:22-alpine' - &generator_image 'openapitools/openapi-generator-cli:v7.13.0@sha256:52b4b2bb6129b086b0f12614e98c884d378a84349fa114b07e20515702a793c5' - &environment HTTP_PROXY: @@ -21,20 +22,17 @@ steps: ref: HEAD path: libre-graph-api-go remote: https://github.com/opencloud-eu/libre-graph-api-go.git + compile-spec: + image: *node_image + environment: *environment + commands: + - sh scripts/compile-spec.sh generate-go: image: *generator_image environment: *environment commands: - rm -Rf libre-graph-api-go/* - - '/usr/local/bin/docker-entrypoint.sh generate - --enable-post-process-file - -i api/openapi-spec/v1.0.yaml - --additional-properties=packageName=libregraph - --git-user-id=opencloud-eu - --git-repo-id=libre-graph-api-go - -g go - -o libre-graph-api-go - --api-name-suffix Api' + - sh scripts/generate-go.sh - cp LICENSE libre-graph-api-go/LICENSE diff: image: *alpine_image diff --git a/.woodpecker/build-php.yaml b/.woodpecker/build-php.yaml index d7584f6..398204c 100644 --- a/.woodpecker/build-php.yaml +++ b/.woodpecker/build-php.yaml @@ -1,6 +1,7 @@ --- variables: - &alpine_image 'owncloudci/alpine:latest' + - &node_image 'node:22-alpine' - &generator_image 'openapitools/openapi-generator-cli:v7.13.0@sha256:52b4b2bb6129b086b0f12614e98c884d378a84349fa114b07e20515702a793c5' - &environment HTTP_PROXY: @@ -21,20 +22,17 @@ steps: ref: HEAD path: libre-graph-api-php remote: https://github.com/opencloud-eu/libre-graph-api-php.git + compile-spec: + image: *node_image + environment: *environment + commands: + - sh scripts/compile-spec.sh generate-php: image: *generator_image environment: *environment commands: - rm -Rf libre-graph-api-php/* - - '/usr/local/bin/docker-entrypoint.sh generate - --enable-post-process-file - -i api/openapi-spec/v1.0.yaml - --additional-properties=packageName=libregraph - --git-user-id=opencloud-eu - --git-repo-id=libre-graph-api-php - -g php-nextgen - -t templates/php-nextgen - -o libre-graph-api-php' + - sh scripts/generate-php.sh - cp LICENSE libre-graph-api-php/LICENSE diff: image: *alpine_image diff --git a/.woodpecker/build-typescript-axios.yaml b/.woodpecker/build-typescript-axios.yaml index b3a0cd8..b278e5f 100644 --- a/.woodpecker/build-typescript-axios.yaml +++ b/.woodpecker/build-typescript-axios.yaml @@ -1,6 +1,7 @@ --- variables: - &alpine_image 'owncloudci/alpine:latest' + - &node_image 'node:22-alpine' - &generator_image 'openapitools/openapi-generator-cli:v7.13.0@sha256:52b4b2bb6129b086b0f12614e98c884d378a84349fa114b07e20515702a793c5' - &environment HTTP_PROXY: @@ -21,20 +22,17 @@ steps: ref: HEAD path: libre-graph-api-typescript-axios remote: https://github.com/opencloud-eu/libre-graph-api-typescript-axios.git + compile-spec: + image: *node_image + environment: *environment + commands: + - sh scripts/compile-spec.sh generate-typescript-axios: image: *generator_image environment: *environment commands: - rm -Rf libre-graph-api-typescript-axios/* - - '/usr/local/bin/docker-entrypoint.sh generate - --enable-post-process-file - -i api/openapi-spec/v1.0.yaml - --additional-properties=packageName=libregraph - --git-user-id=opencloud-eu - --git-repo-id=libre-graph-api-typescript-axios - -g typescript-axios - -o libre-graph-api-typescript-axios - --api-name-suffix Api' + - sh scripts/generate-typescript-axios.sh - cp LICENSE libre-graph-api-typescript-axios/LICENSE diff: image: *alpine_image diff --git a/.woodpecker/docs.yaml b/.woodpecker/docs.yaml index a502744..6c50143 100644 --- a/.woodpecker/docs.yaml +++ b/.woodpecker/docs.yaml @@ -1,10 +1,17 @@ --- +variables: + - &node_image 'node:22-alpine' + when: - event: push branch: ${CI_REPO_DEFAULT_BRANCH} - event: [ manual, tag] steps: + - name: compile-spec + image: *node_image + commands: + - sh scripts/compile-spec.sh - name: deploy-staging image: debian:latest environment: diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7fbf87b --- /dev/null +++ b/Makefile @@ -0,0 +1,53 @@ +# libre-graph-api local build entry points. +# +# `make` (or `make spec`) compiles the TypeSpec sources under spec/ into +# api/openapi-spec/v1.0.yaml. Per-language client generation targets pull the +# matching openapi-generator-cli image straight out of the woodpecker pipeline +# file for that language, so renovatebot remains the single source of truth +# for the generator version. + +SPEC := api/openapi-spec/v1.0.yaml +NODE_IMAGE := node:22-alpine + +# Extract `openapitools/openapi-generator-cli:@sha256:` from the +# .woodpecker pipeline file for the given language. +# $(call generator_image,go) +# $(call generator_image,typescript-axios) +generator_image = $(shell grep -oE "openapitools/openapi-generator-cli:[A-Za-z0-9._@:-]+" .woodpecker/build-$(1).yaml | head -n1) + +DOCKER_RUN = docker run --rm \ + --user $(shell id -u):$(shell id -g) \ + -v "$(CURDIR):/work" \ + -w /work + +.PHONY: all spec go typescript-axios php cpp-qt-client clean help + +all: spec + +help: + @printf '%s\n' \ + 'Targets:' \ + ' make spec compile TypeSpec sources -> $(SPEC)' \ + ' make go generate Go client into build/clients/go' \ + ' make typescript-axios generate TypeScript-Axios client into build/clients/typescript-axios' \ + ' make php generate PHP client into build/clients/php' \ + ' make cpp-qt-client generate C++/Qt client into build/clients/cpp-qt-client' \ + ' make clean wipe build artefacts and the generated spec' + +spec $(SPEC): + $(DOCKER_RUN) $(NODE_IMAGE) sh scripts/compile-spec.sh + +go: $(SPEC) + $(DOCKER_RUN) -e OUTPUT_DIR=build/clients/go $(call generator_image,go) sh scripts/generate-go.sh + +typescript-axios: $(SPEC) + $(DOCKER_RUN) -e OUTPUT_DIR=build/clients/typescript-axios $(call generator_image,typescript-axios) sh scripts/generate-typescript-axios.sh + +php: $(SPEC) + $(DOCKER_RUN) -e OUTPUT_DIR=build/clients/php $(call generator_image,php) sh scripts/generate-php.sh + +cpp-qt-client: $(SPEC) + $(DOCKER_RUN) -e OUTPUT_DIR=build/clients/cpp-qt-client $(call generator_image,cpp-qt) sh scripts/generate-cpp-qt-client.sh + +clean: + rm -rf build/ spec/build/ $(SPEC) diff --git a/api/openapi-spec/v1.0.yaml b/api/openapi-spec/v1.0.yaml deleted file mode 100644 index 534c09e..0000000 --- a/api/openapi-spec/v1.0.yaml +++ /dev/null @@ -1,6234 +0,0 @@ -openapi: 3.0.1 -info: - title: Libre Graph API - description: Libre Graph is a free API for cloud collaboration inspired by the MS Graph API. - version: v1.0.8 - license: - name: Apache 2.0 - url: https://www.apache.org/licenses/LICENSE-2.0.html -servers: - - url: https://localhost:9200/graph - description: OpenCloud Development Setup -paths: - '/v1.0/me': - patch: - tags: - - me.user - summary: Update the current user - operationId: UpdateOwnUser - requestBody: - description: New user values - content: - application/json: - schema: - $ref: '#/components/schemas/userUpdate' - examples: - change preferred language: - value: - preferredLanguage: "en" - responses: - '200': - description: Success - content: - application/json: - schema: - $ref: '#/components/schemas/user' - default: - $ref: '#/components/responses/error' - get: - tags: - - me.user - summary: Get current user - operationId: GetOwnUser - parameters: - - name: $expand - in: query - description: Expand related entities - style: form - explode: false - schema: - uniqueItems: true - type: array - items: - enum: - - memberOf - type: string - responses: - '200': - description: Retrieved entity - content: - application/json: - schema: - $ref: '#/components/schemas/user' - default: - $ref: '#/components/responses/error' - x-ms-docs-operation-type: operation - /v1.0/me/changePassword: - post: - tags: - - me.changepassword - summary: Change your own password - operationId: ChangeOwnPassword - requestBody: - description: Password change request - content: - application/json: - schema: - title: Password change - type: object - required: - - currentPassword - - newPassword - properties: - currentPassword: - type: string - newPassword: - type: string - required: true - responses: - '204': - description: Success - default: - $ref: '#/components/responses/error' - x-ms-docs-operation-type: operation - /v1.0/me/photo/$value: - get: - tags: - - me.photo - summary: Get the current user's profile photo - operationId: GetOwnUserPhoto - responses: - '200': - description: Retrieved profile photo - content: - image/jpeg: - schema: - type: string - format: binary - default: - $ref: '#/components/responses/error' - put: - tags: - - me.photo - summary: Update the current user's profile photo - operationId: UpdateOwnUserPhotoPut - requestBody: - description: New user photo - content: - image/jpeg: - schema: - type: string - format: binary - responses: - '204': - description: Success - default: - $ref: '#/components/responses/error' - patch: - tags: - - me.photo - summary: Update the current user's profile photo - operationId: UpdateOwnUserPhotoPatch - requestBody: - description: New user photo - content: - image/jpeg: - schema: - type: string - format: binary - responses: - '204': - description: Success - default: - $ref: '#/components/responses/error' - delete: - tags: - - me.photo - summary: Delete the current user's profile photo - operationId: DeleteOwnUserPhoto - responses: - '204': - description: Success - default: - $ref: '#/components/responses/error' - '/v1beta1/extensions/org.libregraph/activities': - get: - tags: - - activities - summary: Get activities - operationId: GetActivities - parameters: - - in: query - name: kql - schema: - type: string - example: resourceid:a0ca6a90-a365-4782-871e-d44447bbc668$a0ca6a90-a365-4782-871e-d44447bbc668 depth:2 - responses: - '200': - description: Found activities - content: - application/json: - schema: - title: Collection of activities - type: object - properties: - value: - type: array - items: - $ref: '#/components/schemas/activity' - examples: - item activities: - value: - - id: "34646ab6-be32-43c9-89e6-987e0c237e9b" - times: - recordedTime: "2023-09-14T12:27:15" - template: - message: "{user} shared {resource}" - variables: - user: - id: "4c510ada-c86b-4815-8820-42cdf82c3d51" - displayName: "Albert Einstein" - resource: - id: "7f92b0a9-06ad-49dc-890f-0e0a6eb4dea6$e9f01ca3-577f-4d1d-acd4-1cc44149ac25!5fb9f87c-a317-467b-9882-eb9f171564ac" - name: "secret stuff" - default: - $ref: '#/components/responses/error' - '/v1beta1/me/drives': - # $ref is buggy in the openapi-generator v7.0.1 - # $ref: '#/paths/~1v1.0~1me~1drives' - get: - tags: - - me.drives - summary: Alias for '/v1.0/drives', the difference is that grantedtoV2 is used and roles contain unified roles instead of cs3 roles - operationId: ListMyDrivesBeta - parameters: - - $ref: '#/components/parameters/orderby' - - $ref: '#/components/parameters/drivesFilter' - - $ref: '#/components/parameters/drivesExpand' - - $ref: '#/components/parameters/drivesSelect' - responses: - '200': - description: Retrieved spaces - content: - application/json: - schema: - title: Collection of drives - type: object - properties: - value: - type: array - items: - $ref: '#/components/schemas/drive' - maxItems: 100 - '@odata.nextLink': - type: string - links: - root: - operationId: HomeGetRoot - default: - $ref: '#/components/responses/error' - x-ms-pageable: - nextLinkName: '@odata.nextLink' - operationName: listMore - x-ms-docs-operation-type: operation - /v1.0/me/drives: - get: - tags: - - me.drives - summary: Get all drives where the current user is a regular member of - operationId: ListMyDrives - parameters: - - $ref: '#/components/parameters/orderby' - - $ref: '#/components/parameters/drivesFilter' - responses: - '200': - description: Retrieved spaces - content: - application/json: - schema: - title: Collection of drives - type: object - properties: - value: - type: array - items: - $ref: '#/components/schemas/drive' - maxItems: 100 - '@odata.nextLink': - type: string - links: - root: - operationId: HomeGetRoot - default: - $ref: '#/components/responses/error' - x-ms-pageable: - nextLinkName: '@odata.nextLink' - operationName: listMore - x-ms-docs-operation-type: operation - '/v1beta1/drives': - # $ref is buggy in the openapi-generator v7.0.1 - # $ref: '#/paths/~1v1.0~1drives' - get: - tags: - - drives.GetDrives - summary: Alias for '/v1.0/drives', the difference is that grantedtoV2 is used and roles contain unified roles instead of cs3 roles - operationId: ListAllDrivesBeta - parameters: - - $ref: '#/components/parameters/orderby' - - $ref: '#/components/parameters/drivesFilter' - - $ref: '#/components/parameters/drivesExpand' - - $ref: '#/components/parameters/drivesSelect' - responses: - '200': - description: Retrieved spaces - content: - application/json: - schema: - title: Collection of drives - type: object - properties: - value: - type: array - items: - $ref: '#/components/schemas/drive' - links: - root: - operationId: GetRoot - default: - $ref: '#/components/responses/error' - x-ms-docs-operation-type: operation - '/v1.0/drives': - get: - tags: - - drives.GetDrives - summary: Get all available drives - operationId: ListAllDrives - parameters: - - $ref: '#/components/parameters/orderby' - - $ref: '#/components/parameters/drivesFilter' - responses: - '200': - description: Retrieved spaces - content: - application/json: - schema: - title: Collection of drives - type: object - properties: - value: - type: array - items: - $ref: '#/components/schemas/drive' - links: - root: - operationId: GetRoot - default: - $ref: '#/components/responses/error' - x-ms-docs-operation-type: operation - post: - tags: - - drives - summary: Create a new drive of a specific type - operationId: CreateDrive - requestBody: - description: New space property values - content: - application/json: - schema: - $ref: '#/components/schemas/drive' - examples: - multiple values: - value: - name: Mars - quota: - total: 1000000000 - description: Team space mars project - minimal: - value: - name: Venus - required: true - responses: - '201': - description: Created - content: - application/json: - schema: - $ref: '#/components/schemas/drive' - default: - $ref: '#/components/responses/error' - x-ms-docs-operation-type: operation - '/v1.0/drives/{drive-id}': - get: - tags: - - drives - summary: Get drive by id - operationId: GetDrive - parameters: - - name: drive-id - in: path - description: 'key: id of drive' - required: true - schema: - type: string - x-ms-docs-key-type: drive - - $ref: '#/components/parameters/drivesSelect' - responses: - '200': - description: Retrieved drive - content: - application/json: - schema: - $ref: '#/components/schemas/drive' - links: - root: - operationId: GetRoot - parameters: - drive-id: $request.path.drive-id - default: - $ref: '#/components/responses/error' - x-ms-docs-operation-type: operation - patch: - tags: - - drives - summary: Update the drive - operationId: UpdateDrive - parameters: - - name: drive-id - in: path - description: 'key: id of drive' - required: true - schema: - type: string - x-ms-docs-key-type: drive - requestBody: - description: New space values - content: - application/json: - schema: - $ref: '#/components/schemas/driveUpdate' - examples: - change quota: - value: - quota: - total: 1000000000 - change name: - value: - name: Physics - change description: - value: - description: Marketing team collaboration area - change drive image: - value: - special: - id: 408f26e3-d6d8-4aa4-bd5c-3fd6f5032b51$aba2bafc-e1a0-421e-a37a-9acf5f655f83!14fd3432-e8d9-42a7-9446-2e3fd621afca - specialFolder: - name: image - change drive readme: - value: - special: - id: 408f26e3-d6d8-4aa4-bd5c-3fd6f5032b51$aba2bafc-e1a0-421e-a37a-9acf5f655f83!14fd3432-e8d9-42a7-9446-2e3fd621afca - specialFolder: - name: readme - required: true - responses: - '200': - description: Success - content: - application/json: - schema: - $ref: '#/components/schemas/drive' - default: - $ref: '#/components/responses/error' - x-ms-docs-operation-type: operation - delete: - tags: - - drives - summary: Delete a specific space - operationId: DeleteDrive - parameters: - - name: drive-id - in: path - description: 'key: id of drive' - required: true - schema: - type: string - x-ms-docs-key-type: drive - - name: If-Match - in: header - description: ETag - schema: - type: string - responses: - '204': - description: Success - default: - $ref: '#/components/responses/error' - x-ms-docs-operation-type: operation - '/v1beta1/drives/{drive-id}/root/children': - post: - tags: - - drives.root - summary: Create a drive item - operationId: CreateDriveItem - description: | - You can use the root childrens endpoint to mount a remoteItem in the share jail. The `@client.synchronize` property of the `driveItem` in the [sharedWithMe](#/me.drive/ListSharedWithMe) endpoint will change to true. - parameters: - - name: drive-id - in: path - description: 'key: id of drive' - required: true - schema: - type: string - example: a0ca6a90-a365-4782-871e-d44447bbc668$a0ca6a90-a365-4782-871e-d44447bbc668 - x-ms-docs-key-type: drive - requestBody: - description: In the request body, provide a JSON object with the following parameters. For mounting a share the necessary remoteItem id and permission id can be taken from the [sharedWithMe](#/me.drive/ListSharedWithMe) endpoint. - content: - application/json: - schema: - $ref: '#/components/schemas/driveItem' - examples: - mount a shared remoteId: - value: - name: Einsteins project share - remoteItem: - id: a-storage-provider-id$a-space-id!a-node-id - responses: - '200': - description: Response - content: - application/json: - schema: - $ref: '#/components/schemas/driveItem' - example: - name: Einsteins project share - id: a0ca6a90-a365-4782-871e-d44447bbc668$a0ca6a90-a365-4782-871e-d44447bbc668!share-id - remoteItem: - id: a-storage-provider-id$a-space-id!a-node-id - name: Project - parentReference: - driveID: a0ca6a90-a365-4782-871e-d44447bbc668$a0ca6a90-a365-4782-871e-d44447bbc668 - id: a0ca6a90-a365-4782-871e-d44447bbc668$a0ca6a90-a365-4782-871e-d44447bbc668!a0ca6a90-a365-4782-871e-d44447bbc668 - default: - $ref: '#/components/responses/error' - x-ms-docs-operation-type: operation - '/v1beta1/drives/{drive-id}/root/createLink': - post: - tags: - - drives.root - summary: Create a sharing link for the root item of a Drive - operationId: CreateLinkSpaceRoot - description: | - You can use the createLink action to share a driveItem via a sharing link. - - The response will be a permission object with the link facet containing the created link details. - - ## Link types - - For now, The following values are allowed for the type parameter. - - | Value | Display name | Description | - | -------------- | ----------------- | --------------------------------------------------------------- | - | view | View | Creates a read-only link to the driveItem. | - | upload | Upload | Creates a read-write link to the folder driveItem. | - | edit | Edit | Creates a read-write link to the driveItem. | - | createOnly | File Drop | Creates an upload-only link to the folder driveItem. | - | blocksDownload | Secure View | Creates a read-only link that blocks download to the driveItem. | - - parameters: - - name: drive-id - in: path - description: 'key: id of drive' - required: true - schema: - type: string - x-ms-docs-key-type: drive - requestBody: - description: In the request body, provide a JSON object with the following parameters. - content: - application/json: - schema: - $ref: '#/components/schemas/driveItemCreateLink' - examples: - create viewer link: - value: - type: "view" - create editor link with custom display name: - value: - type: "edit" - displayName: "Homework" - create editor link with expiry: - value: - type: "edit" - expirationDateTime: "2018-07-15T14:00:00.000Z" - responses: - '200': - description: Response - content: - application/json: - schema: - $ref: '#/components/schemas/permission' - examples: - create viewer link: - value: - - id: "7181275c-557e-4adf-8e44-abdcf5389b6a" - link: - type: "view" - webUrl: "https://cloud.example.org/s/YlaySZdHCRHUDeE" - create editor link with custom display name: - value: - - id: "7181275c-557e-4adf-8e44-abdcf5389b6a" - link: - type: "edit" - displayName: "Homework" - webUrl: "https://cloud.example.org/s/fdleMyGeerkIZTJ" - create editor link with expiry: - value: - - id: "a373c812-d786-42ae-a65f-ed87ee4abd49" - link: - type: "edit" - webUrl: "https://cloud.example.org/s/MlnTDkhcBlihNzO" - '207': - # FIXME describe partial success response - description: Partial success response TODO - default: - $ref: '#/components/responses/error' - # TODO add SendNotification errors as per https://learn.microsoft.com/en-us/graph/api/driveitem-invite?view=graph-rest-beta&tabs=http#sendnotification-errors - x-ms-docs-operation-type: operation - '/v1beta1/drives/{drive-id}/root/invite': - post: - tags: - - drives.root - summary: Send a sharing invitation - operationId: InviteSpaceRoot - description: | - Sends a sharing invitation for the root of a `drive`. A sharing invitation provides permissions to the - recipients and optionally sends them an email with a sharing link. - - The response will be a permission object with the grantedToV2 property containing the created grant details. - - ## Roles property values - For now, roles are only identified by a uuid. There are no hardcoded aliases like `read` or `write` because role actions can be completely customized. - # FIXME we should add isBuiltIn and isEnabled to unifiedRoleDefinition so we can use speaking role ids in examples and establish a well defined set of roles. - # By throwing around only uuids we increase the risk of someone choosing an ambiguous or misleading displayname. We will have to define a set of default - # sharing permissions. For those we should use speaking ids. - # - # For now, the following roles are considered built in: - # - # | Role | Value | Description | - # | ------- | ------- | --------------------------------------------------------------------------------------------------------------------- | - # | Viewer | `read` | Provides the ability to read the metadata and contents of the item. | - # | Editor | `write` | Provides the ability to read and modify the metadata and contents of the item. | - # | Manager | `owner` | For project drives this represents the Manager role to mimic ms graph | - # - # More built in roles will be added with a speaking value but clients **MUST** treat the value as an - # opaque identifier as custom roles will be using UUIDs. - - parameters: - - name: drive-id - in: path - description: 'key: id of drive' - required: true - schema: - type: string - x-ms-docs-key-type: drive - requestBody: - description: In the request body, provide a JSON object with the following parameters. To create a custom role submit a list of actions instead of roles. - content: - application/json: - schema: - $ref: '#/components/schemas/driveItemInvite' - examples: - send viewer invite: - value: - recipients: - - '@libre.graph.recipient.type': user - objectId: "4c510ada-c86b-4815-8820-42cdf82c3d51" - roles: [ "b1e2218d-eef8-4d4c-b82d-0f1a1b48f3b5" ] - send viewer invite to multiple recipients: - value: - recipients: - - '@libre.graph.recipient.type': user - objectId: "4c510ada-c86b-4815-8820-42cdf82c3d51" - - '@libre.graph.recipient.type': user - objectId: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c" - roles: [ "b1e2218d-eef8-4d4c-b82d-0f1a1b48f3b5" ] - send editor invite with expiry: - value: - recipients: - - '@libre.graph.recipient.type': user - objectId: "4c510ada-c86b-4815-8820-42cdf82c3d51" - roles: [ "fb6c3e19-e378-47e5-b277-9732f9de6e21" ] - expirationDateTime: "2018-07-15T14:00:00.000Z" - send manager invite to group: - description: "Sharing with a groups requires setting the @libre.graph.recipient.type annotation." - value: - recipients: - - '@libre.graph.recipient.type': group - objectId: "167cbee2-0518-455a-bfb2-031fe0621e5d" - roles: [ "312c0871-5ef7-4b3a-85b6-0e4074c64049" ] - send invite to user granting custom actions: - description: "A Custom role is just a collection of actions, in this case allowing the user to stat and move driveItems" - value: - recipients: - - objectId: "4c510ada-c86b-4815-8820-42cdf82c3d51" - '@libre.graph.permissions.actions': [ "libre.graph/driveItem/basic/read", "libre.graph/driveItem/path/update" ] - responses: - '200': - description: Response - content: - application/json: - schema: - title: Collection of permissions - type: object - properties: - value: - type: array - items: - $ref: '#/components/schemas/permission' - examples: - send viewer invite: - value: - - id: "34646ab6-be32-43c9-89e6-987e0c237e9b" - roles: [ "b1e2218d-eef8-4d4c-b82d-0f1a1b48f3b5" ] - grantedToV2: - - user: - id: "4c510ada-c86b-4815-8820-42cdf82c3d51" - displayName: "Albert Einstein" - send viewer invite to multiple recipients: - description: multiple permissions are returned as each has received a dedicated sharing id - value: - - id: "81d5bad3-3eff-410a-a2ea-eda2d14d4474" - roles: [ "b1e2218d-eef8-4d4c-b82d-0f1a1b48f3b5" ] - grantedToV2: - - user: - id: "4c510ada-c86b-4815-8820-42cdf82c3d51" - displayName: "Albert Einstein" - - id: "b470677e-a7f5-4304-8ef5-f5056a21fff1" - roles: [ "b1e2218d-eef8-4d4c-b82d-0f1a1b48f3b5" ] - grantedToV2: - - user: - id: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c" - displayName: "Marie Skłodowska Curie" - send editor invite with expiry: - value: - - id: "453b02be-4ec2-4e7d-b576-09fc153de812" - roles: [ "fb6c3e19-e378-47e5-b277-9732f9de6e21" ] - grantedToV2: - - user: - id: "4c510ada-c86b-4815-8820-42cdf82c3d51" - displayName: "Albert Einstein" - expirationDateTime: "2018-07-15T14:00:00.000Z" - send manager invite to group: - value: - - id: "86765c0d-3905-444a-9b07-76201f8cf7df" - roles: [ "312c0871-5ef7-4b3a-85b6-0e4074c64049" ] - grantedToV2: - - group: - id: "167cbee2-0518-455a-bfb2-031fe0621e5d" - displayName: "Philosophy Haters" - send invite to user granting custom actions: - value: - - id: "c42b5cbd-2d65-42cf-b0b6-fb6d2b762256" - grantedToV2: - - user: - id: "4c510ada-c86b-4815-8820-42cdf82c3d51" - displayName: "Albert Einstein" - '@libre.graph.permissions.actions': [ "libre.graph/driveItem/basic/read", "libre.graph/driveItem/path/update" ] - '207': - # FIXME describe partial success response - description: Partial success response TODO - '400': - description: Bad request - content: - application/json: - schema: - $ref: '#/components/schemas/odata.error' - examples: - secureSharingInvalidRequest: - description: If the recipient was given via email, but does not belong to a known user account. - value: - - error: - - code: invalidRequest - message: Some users in the request cannot be invited securely. - innerError: - - code: secureSharingInvalidRequest - date: "2023-09-14T12:27:15" - request-id: b49f582d-9a0f-4731-ab0c-b9e50246ef58 - client-request-id: 832c7206-1a40-395c-e0d9-e85649b8b0b0 - default: - $ref: '#/components/responses/error' - # TODO add SendNotification errors as per https://learn.microsoft.com/en-us/graph/api/driveitem-invite?view=graph-rest-beta&tabs=http#sendnotification-errors - x-ms-docs-operation-type: operation - '/v1beta1/drives/{drive-id}/root/permissions': - get: - tags: - - drives.root - summary: List the effective permissions on the root item of a drive. - operationId: ListPermissionsSpaceRoot - description: | - The permissions collection includes potentially sensitive information and may not be available for every caller. - - * For the owner of the item, all sharing permissions will be returned. This includes co-owners. - * For a non-owner caller, only the sharing permissions that apply to the caller are returned. - * Sharing permission properties that contain secrets (e.g. `webUrl`) are only returned for callers that are able to create the sharing permission. - - All permission objects have an `id`. A permission representing - * a link has the `link` facet filled with details. - * a share has the `roles` property set and the `grantedToV2` property filled with the grant recipient details. - - # TODO hm, the ms graph api seems to always fill the `roles` property to indicate the actions granted by the link. - # | Value | Display name | Description | role? | - # | -------------- | ----------------- | --------------------------------------------------------------- | -------------- | - # | view | View | Creates a read-only link to the driveItem. | viewer | - # | upload | Upload | Creates a read-write link to the folder driveItem. | uploader | - # | edit | Edit | Creates a read-write link to the driveItem. | editor | - # | createOnly | File Drop | Creates an upload-only link to the folder driveItem. | createOnly | - # | blocksDownload | Secure View | Creates a read-only link that blocks download to the driveItem. | blocksDownload | - # AFAICT we will have to define the actions granted by a link type when we start to implement this - # but e.g. a createOnly role and blocksDownload should not show up in the drop down for sharing files internally ... - # when we need to hardcode this anyway ... we should indicate that by using the buildIn flag ... but how do we prevent some roles from showing up in the internal sharing dialog ... - # for now leave out the roles property and hardcode the actions allowed by the link type - - parameters: - - name: drive-id - in: path - description: 'key: id of drive' - required: true - schema: - type: string - x-ms-docs-key-type: drive - - $ref: '#/components/parameters/permissionsFilter' - - $ref: '#/components/parameters/permissionsSelect' - - $ref: '#/components/parameters/count' - - $ref: '#/components/parameters/top' - responses: - '200': - description: Retrieved resource - content: - application/json: - schema: - title: Collection of permissions with allowed values - type: object - properties: - '@libre.graph.permissions.roles.allowedValues': - type: array - description: A list of role definitions that can be chosen for the resource. - items: - $ref: '#/components/schemas/unifiedRoleDefinition' - '@libre.graph.permissions.actions.allowedValues': - type: array - description: | - A list of actions that can be chosen for a custom role. - - Following the CS3 API we can represent the CS3 permissions by mapping them to driveItem properties or relations like this: - | [CS3 ResourcePermission](https://cs3org.github.io/cs3apis/#cs3.storage.provider.v1beta1.ResourcePermissions) | action | comment | - | ------------------------------------------------------------------------------------------------------------ | ------ | ------- | - | `stat` | `libre.graph/driveItem/basic/read` | `basic` because it does not include versions or trashed items | - | `get_quota` | `libre.graph/driveItem/quota/read` | read only the `quota` property | - | `get_path` | `libre.graph/driveItem/path/read` | read only the `path` property | - | `move` | `libre.graph/driveItem/path/update` | allows updating the `path` property of a CS3 resource | - | `delete` | `libre.graph/driveItem/standard/delete` | `standard` because deleting is a common update operation | - | `list_container` | `libre.graph/driveItem/children/read` | | - | `create_container` | `libre.graph/driveItem/children/create` | | - | `initiate_file_download` | `libre.graph/driveItem/content/read` | `content` is the property read when initiating a download | - | `initiate_file_upload` | `libre.graph/driveItem/upload/create` | `uploads` are a separate property. postprocessing creates the `content` | - | `add_grant` | `libre.graph/driveItem/permissions/create` | | - | `list_grant` | `libre.graph/driveItem/permissions/read` | | - | `update_grant` | `libre.graph/driveItem/permissions/update` | | - | `remove_grant` | `libre.graph/driveItem/permissions/delete` | | - | `deny_grant` | `libre.graph/driveItem/permissions/deny` | uses a non CRUD action `deny` | - | `list_file_versions` | `libre.graph/driveItem/versions/read` | `versions` is a `driveItemVersion` collection | - | `restore_file_version` | `libre.graph/driveItem/versions/update` | the only `update` action is restore | - | `list_recycle` | `libre.graph/driveItem/deleted/read` | reading a driveItem `deleted` property implies listing | - | `restore_recycle_item` | `libre.graph/driveItem/deleted/update` | the only `update` action is restore | - | `purge_recycle` | `libre.graph/driveItem/deleted/delete` | allows purging deleted `driveItems` | - items: - type: string - value: - type: array - items: - $ref: '#/components/schemas/permission' - # maxItems: 100 - '@odata.count': - type: integer - description: The total number of permissions available, only present if the `count` query parameter is set to true. - #'@odata.nextLink': - # type: string - example: - '@libre.graph.permissions.roles.allowedValues': - - id: b1e2218d-eef8-4d4c-b82d-0f1a1b48f3b5 - description: "Allows reading the shared file or folder" - displayName: Viewer - # we don't want to reproduce all the individual role permissions, that is what the /roleManagement/permissions/roleDefinitions endpoint is for - # to build a custom role - #rolePermissions: - # - allowedResourceActions: - # - libre.graph/driveItem/basic/read - # - libre.graph/driveItem/permissions/read - # - libre.graph/driveItem/upload/create - # condition: "exists @Resource.File" - '@libre.graph.weight': 1 - - id: fb6c3e19-e378-47e5-b277-9732f9de6e21 - description: "Allows reading and writing the shared file or folder" - displayName: Editor - #rolePermissions: - # - allowedResourceActions: - # - libre.graph/driveItem/standard/allTasks - # condition: "exists @Resource.File" - '@libre.graph.weight': 2 - - id: 312c0871-5ef7-4b3a-85b6-0e4074c64049 - description: "Allows managing a space" - displayName: Manager - #rolePermissions: - # - allowedResourceActions: - # - libre.graph/drive/standard/allTasks - # condition: "exists @Resource.File" - '@libre.graph.weight': 3 - - id: 4916f47e-66d5-49bb-9ac9-748ad00334b - description: "Allows creating new files" - displayName: File Drop - #rolePermissions: - # - allowedResourceActions: - # libre.graph/driveItem/upload/create - # condition: "exists @Resource.File" - '@libre.graph.weight': 4 - '@libre.graph.permissions.actions.allowedValues': [ - "libre.graph/driveItem/basic/read", - "libre.graph/driveItem/permissions/read", - "libre.graph/driveItem/upload/create", - "libre.graph/driveItem/standard/allTasks", - "libre.graph/driveItem/upload/create" - ] - - value: - - id: "67445fde-a647-4dd4-b015-fc5dafd2821d" - link: - type: "view" - webUrl: "https://cloud.example.org/s/fhGBMIkKFEHWysj" - - - id: "34646ab6-be32-43c9-89e6-987e0c237e9b" - roles: [ "b1e2218d-eef8-4d4c-b82d-0f1a1b48f3b5" ] - grantedToV2: - - user: - id: "4c510ada-c86b-4815-8820-42cdf82c3d51" - displayName: "Albert Einstein" - - id: "81d5bad3-3eff-410a-a2ea-eda2d14d4474" - roles: [ "b1e2218d-eef8-4d4c-b82d-0f1a1b48f3b5" ] - grantedToV2: - - user: - id: "4c510ada-c86b-4815-8820-42cdf82c3d51" - displayName: "Albert Einstein" - - id: "b470677e-a7f5-4304-8ef5-f5056a21fff1" - roles: [ "b1e2218d-eef8-4d4c-b82d-0f1a1b48f3b5" ] - grantedToV2: - - user: - id: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c" - displayName: "Marie Skłodowska Curie" - - id: "453b02be-4ec2-4e7d-b576-09fc153de812" - roles: [ "fb6c3e19-e378-47e5-b277-9732f9de6e21" ] - grantedToV2: - - user: - id: "4c510ada-c86b-4815-8820-42cdf82c3d51" - displayName: "Albert Einstein" - expirationDateTime: "2018-07-15T14:00:00.000Z" - - id: "86765c0d-3905-444a-9b07-76201f8cf7df" - roles: [ "312c0871-5ef7-4b3a-85b6-0e4074c64049" ] - grantedToV2: - - group: - id: "167cbee2-0518-455a-bfb2-031fe0621e5d" - displayName: "Philosophy Haters" - - id: "c42b5cbd-2d65-42cf-b0b6-fb6d2b762256" - grantedToV2: - - user: - id: "4c510ada-c86b-4815-8820-42cdf82c3d51" - displayName: "Albert Einstein" - '@libre.graph.permissions.actions': [ "libre.graph/driveItem/basic/read", "libre.graph/driveItem/path/update" ] - default: - $ref: '#/components/responses/error' - x-ms-docs-operation-type: operation - '/v1beta1/drives/{drive-id}/root/permissions/{perm-id}': - get: - tags: - - drives.root - summary: Get a single sharing permission for the root item of a drive - operationId: GetPermissionSpaceRoot - description: | - Return the effective sharing permission for a particular permission resource. - - # TODO do we need to implement inherited permissions from the start? - # Effective permissions of an item can come from two sources: permissions set directly on the item itself or permissions that are inherited from the item's ancestors. - # - # Callers can differentiate if the permission is inherited or not by checking the `inheritedFrom` property. This property is an ItemReference resource referencing the ancestor that the permission is inherited from. - # - parameters: - - name: drive-id - in: path - description: 'key: id of drive' - required: true - schema: - type: string - x-ms-docs-key-type: drive - - name: perm-id - in: path - description: 'key: id of permission' - required: true - schema: - type: string - x-ms-docs-key-type: permission - responses: - '200': - description: Retrieved resource - content: - application/json: - schema: - $ref: '#/components/schemas/permission' - examples: - edit permission: - value: - id: "81d5bad3-3eff-410a-a2ea-eda2d14d4474" - roles: [ "write" ] - grantedToV2: - - user: - id: "4c510ada-c86b-4815-8820-42cdf82c3d51" - displayName: "Albert Einstein" - default: - $ref: '#/components/responses/error' - x-ms-docs-operation-type: operation - patch: - tags: - - drives.root - summary: Update sharing permission - operationId: UpdatePermissionSpaceRoot - description: | - Update the properties of a sharing permission by patching the permission resource. - - Only the `roles`, `expirationDateTime` and `password` properties can be modified this way. - parameters: - - name: drive-id - in: path - description: 'key: id of drive' - required: true - schema: - type: string - x-ms-docs-key-type: drive - - name: perm-id - in: path - description: 'key: id of permission' - required: true - schema: - type: string - x-ms-docs-key-type: permission - #- name: If-Match - # in: header - # description: | - # If this request header is included and the eTag (or cTag) provided does not match the current tag on the item, - # a `412 Precondition Failed`` response is returned and the item will not be updated. - # schema: - # type: string - requestBody: - description: New property values - content: - application/json: - schema: - $ref: '#/components/schemas/permission' - examples: - update link: - description: Updating a public link is done by setting the `type` property of the permission link. - value: - link: - type: edit - update invite: - description: Updating an invite is done by setting the `roles` property of the permission. - value: - roles: [ "b1e2218d-eef8-4d4c-b82d-0f1a1b48f3b5" ] - required: true - responses: - '200': - description: Updated permission - content: - application/json: - schema: - $ref: '#/components/schemas/permission' - examples: - update link: - value: - id: "a373c812-d786-42ae-a65f-ed87ee4abd49" - link: - type: "edit" - webUrl: "https://cloud.example.org/s/MlnTDkhcBlihNzO" - update invite: - value: - id: "81d5bad3-3eff-410a-a2ea-eda2d14d4474" - roles: [ "b1e2218d-eef8-4d4c-b82d-0f1a1b48f3b5" ] - expirationDateTime: "2018-07-15T14:00:00.000Z" - grantedToV2: - - user: - id: "4c510ada-c86b-4815-8820-42cdf82c3d51" - displayName: "Albert Einstein" - default: - $ref: '#/components/responses/error' - x-ms-docs-operation-type: operation - delete: - tags: - - drives.root - summary: Remove access to a Drive - operationId: DeletePermissionSpaceRoot - description: | - Remove access to the root item of a drive. - - Only sharing permissions that are not inherited can be deleted. The `inheritedFrom` property must be `null`. - parameters: - - name: drive-id - in: path - description: 'key: id of drive' - required: true - schema: - type: string - x-ms-docs-key-type: drive - - name: perm-id - in: path - description: 'key: id of permission' - required: true - schema: - type: string - x-ms-docs-key-type: permission - #- name: If-Match - # in: header - # description: | - # If this request header is included and the eTag (or cTag) provided does not match the current tag on the item, - # a `412 Precondition Failed`` response is returned and the item will not be updated. - # schema: - # type: string - responses: - '204': - description: Success - default: - $ref: '#/components/responses/error' - x-ms-docs-operation-type: operation - '/v1beta1/drives/{drive-id}/root/permissions/{perm-id}/setPassword': - post: - tags: - - drives.root - summary: Set sharing link password for the root item of a drive - operationId: SetPermissionPasswordSpaceRoot - description: | - Set the password of a sharing permission. - - Only the `password` property can be modified this way. - parameters: - - name: drive-id - in: path - description: 'key: id of drive' - required: true - schema: - type: string - x-ms-docs-key-type: drive - - name: perm-id - in: path - description: 'key: id of permission' - required: true - schema: - type: string - x-ms-docs-key-type: permission - #- name: If-Match - # in: header - # description: | - # If this request header is included and the eTag (or cTag) provided does not match the current tag on the item, - # a `412 Precondition Failed`` response is returned and the item will not be updated. - # schema: - # type: string - requestBody: - description: New password value - content: - application/json: - schema: - $ref: '#/components/schemas/sharingLinkPassword' - examples: - set password: - value: - password: "TestPassword123!" - set empty password: - value: - password: "" - required: true - responses: - '200': - description: Updated permission - content: - application/json: - schema: - $ref: '#/components/schemas/permission' - examples: - set password on editor link with custom display name: - value: - - id: "7181275c-557e-4adf-8e44-abdcf5389b6a" - hasPassword: true - expirationDateTime: "2018-07-15T14:00:00.000Z" - link: - type: "edit" - displayName: "Homework" - webUrl: "https://cloud.example.org/s/fdleMyGeerkIZTJ" - set empty password: - value: - - id: "765c9b7b-6ccf-46ff-a659-95501250229c" - hasPassword: false - link: - type: "view" - displayName: "Quick link" - quicklink: true - webUrl: "https://cloud.example.org/s/erjHgjKjkstTesG" - default: - $ref: '#/components/responses/error' - x-ms-docs-operation-type: operation - '/v1beta1/drives/{drive-id}/items/{item-id}': - get: - tags: - - driveItem - summary: Get a DriveItem. - operationId: GetDriveItem - description: | - Get a DriveItem by using its ID. - parameters: - - name: drive-id - in: path - description: "key: id of drive" - required: true - schema: - type: string - example: a0ca6a90-a365-4782-871e-d44447bbc668$a0ca6a90-a365-4782-871e-d44447bbc668 - x-ms-docs-key-type: drive - - name: item-id - in: path - description: "key: id of item" - required: true - schema: - type: string - example: a0ca6a90-a365-4782-871e-d44447bbc668$a0ca6a90-a365-4782-871e-d44447bbc668!share-id - x-ms-docs-key-type: item - - $ref: '#/components/parameters/driveItemSelect' - responses: - "200": - description: Retrieved driveItem - content: - application/json: - schema: - $ref: "#/components/schemas/driveItem" - default: - $ref: "#/components/responses/error" - x-ms-docs-operation-type: operation - patch: - tags: - - driveItem - summary: Update a DriveItem. - operationId: UpdateDriveItem - description: | - Update a DriveItem. - - The request body must include a JSON object with the properties to update. - Only the properties that are provided will be updated. - - Currently it supports updating the following properties: - - * `@UI.Hidden` - Hides the item from the UI. - parameters: - - name: drive-id - in: path - description: 'key: id of drive' - required: true - schema: - type: string - example: a0ca6a90-a365-4782-871e-d44447bbc668$a0ca6a90-a365-4782-871e-d44447bbc668 - x-ms-docs-key-type: drive - - name: item-id - in: path - description: 'key: id of item' - required: true - schema: - type: string - example: a0ca6a90-a365-4782-871e-d44447bbc668$a0ca6a90-a365-4782-871e-d44447bbc668!share-id - x-ms-docs-key-type: item - requestBody: - description: DriveItem properties to update - content: - application/json: - schema: - $ref: '#/components/schemas/driveItem' - examples: - change visibility: - value: - "@UI.Hidden": true - required: true - responses: - '200': - description: Success - content: - application/json: - schema: - $ref: '#/components/schemas/driveItem' - default: - $ref: '#/components/responses/error' - x-ms-docs-operation-type: operation - delete: - tags: - - driveItem - summary: Delete a DriveItem. - operationId: DeleteDriveItem - description: | - Delete a DriveItem by using its ID. - - Deleting items using this method moves the items to the recycle bin instead of permanently deleting the item. - - Mounted shares in the share jail are unmounted. The `@client.synchronize` property of the `driveItem` in the [sharedWithMe](#/me.drive/ListSharedWithMe) endpoint will change to false. - parameters: - - name: drive-id - in: path - description: 'key: id of drive' - required: true - schema: - type: string - example: a0ca6a90-a365-4782-871e-d44447bbc668$a0ca6a90-a365-4782-871e-d44447bbc668 - x-ms-docs-key-type: drive - - name: item-id - in: path - description: 'key: id of item' - required: true - schema: - type: string - example: a0ca6a90-a365-4782-871e-d44447bbc668$a0ca6a90-a365-4782-871e-d44447bbc668!share-id - x-ms-docs-key-type: item - #- name: If-Match - # in: header - # description: | - # If this request header is included and the eTag (or cTag) provided does not match the current tag on the item, - # a `412 Precondition Failed`` response is returned and the item will not be updated. - # schema: - # type: string - responses: - '204': - description: Success - default: - $ref: '#/components/responses/error' - x-ms-docs-operation-type: operation - '/v1beta1/drives/{drive-id}/items/{item-id}/content': - get: - tags: - - driveItem - summary: Download the content of a DriveItem - operationId: GetDriveItemContent - description: | - Download the contents of the primary stream (file) of a driveItem. Only - driveItem objects with a `file` facet can be downloaded. - - The response is a `302 Found` redirecting to a pre-authenticated download - URL for the file. This is the same URL that is returned via the - `@microsoft.graph.downloadUrl` instance annotation on the driveItem when - requested via `$select`. Choose between the two based on whether you - want to call the redirecting `/content` endpoint directly (for example, - with a client that follows redirects automatically) or you want to - inspect / schedule / prefetch the URL yourself via the annotation. - - The pre-authenticated URL is short-lived and does not require an - `Authorization` header. - - To download a partial range of bytes, apply the `Range` header to the - redirect target (the pre-authenticated URL), not to the `/content` - request. - parameters: - - name: drive-id - in: path - description: 'key: id of drive' - required: true - schema: - type: string - x-ms-docs-key-type: drive - - name: item-id - in: path - description: 'key: id of item' - required: true - schema: - type: string - x-ms-docs-key-type: item - responses: - '302': - description: Pre-authenticated redirect to the file content. - headers: - Location: - required: true - schema: - type: string - format: uri - description: The pre-authenticated URL where the content can be downloaded. - '404': - description: The driveItem was not found or is not a file. - content: - application/json: - schema: - $ref: '#/components/schemas/odata.error' - default: - $ref: '#/components/responses/error' - x-ms-docs-operation-type: operation - '/v1beta1/drives/{drive-id}/items/{item-id}/createLink': - post: - tags: - - drives.permissions - summary: Create a sharing link for a DriveItem - operationId: CreateLink - description: | - You can use the createLink action to share a driveItem via a sharing link. - - The response will be a permission object with the link facet containing the created link details. - - ## Link types - - For now, The following values are allowed for the type parameter. - - | Value | Display name | Description | - | -------------- | ----------------- | --------------------------------------------------------------- | - | view | View | Creates a read-only link to the driveItem. | - | upload | Upload | Creates a read-write link to the folder driveItem. | - | edit | Edit | Creates a read-write link to the driveItem. | - | createOnly | File Drop | Creates an upload-only link to the folder driveItem. | - | blocksDownload | Secure View | Creates a read-only link that blocks download to the driveItem. | - - parameters: - - name: drive-id - in: path - description: 'key: id of drive' - required: true - schema: - type: string - x-ms-docs-key-type: drive - - name: item-id - in: path - description: 'key: id of item' - required: true - schema: - type: string - x-ms-docs-key-type: item - requestBody: - description: In the request body, provide a JSON object with the following parameters. - content: - application/json: - schema: - $ref: '#/components/schemas/driveItemCreateLink' - examples: - create viewer link: - value: - type: "view" - create editor link with custom display name: - value: - type: "edit" - displayName: "Homework" - create editor link with expiry: - value: - type: "edit" - expirationDateTime: "2018-07-15T14:00:00.000Z" - responses: - '200': - description: Response - content: - application/json: - schema: - $ref: '#/components/schemas/permission' - examples: - create viewer link: - value: - - id: "7181275c-557e-4adf-8e44-abdcf5389b6a" - link: - type: "view" - webUrl: "https://cloud.example.org/s/YlaySZdHCRHUDeE" - create editor link with custom display name: - value: - - id: "7181275c-557e-4adf-8e44-abdcf5389b6a" - link: - type: "edit" - displayName: "Homework" - webUrl: "https://cloud.example.org/s/fdleMyGeerkIZTJ" - create editor link with expiry: - value: - - id: "a373c812-d786-42ae-a65f-ed87ee4abd49" - link: - type: "edit" - webUrl: "https://cloud.example.org/s/MlnTDkhcBlihNzO" - '207': - # FIXME describe partial success response - description: Partial success response TODO - default: - $ref: '#/components/responses/error' - # TODO add SendNotification errors as per https://learn.microsoft.com/en-us/graph/api/driveitem-invite?view=graph-rest-beta&tabs=http#sendnotification-errors - x-ms-docs-operation-type: operation - '/v1beta1/drives/{drive-id}/items/{item-id}/invite': - post: - tags: - - drives.permissions - summary: Send a sharing invitation - operationId: Invite - description: | - Sends a sharing invitation for a `driveItem`. A sharing invitation provides permissions to the - recipients and optionally sends them an email with a sharing link. - - The response will be a permission object with the grantedToV2 property containing the created grant details. - - ## Roles property values - For now, roles are only identified by a uuid. There are no hardcoded aliases like `read` or `write` because role actions can be completely customized. - # FIXME we should add isBuiltIn and isEnabled to unifiedRoleDefinition so we can use speaking role ids in examples and establish a well defined set of roles. - # By throwing around only uuids we increase the risk of someone choosing an ambiguous or misleading displayname. We will have to define a set of default - # sharing permissions. For those we should use speaking ids. - # - # For now, the following roles are considered built in: - # - # | Role | Value | Description | - # | ------- | ------- | --------------------------------------------------------------------------------------------------------------------- | - # | Viewer | `read` | Provides the ability to read the metadata and contents of the item. | - # | Editor | `write` | Provides the ability to read and modify the metadata and contents of the item. | - # | Manager | `owner` | For project drives this represents the Manager role to mimic ms graph | - # - # More built in roles will be added with a speaking value but clients **MUST** treat the value as an - # opaque identifier as custom roles will be using UUIDs. - - parameters: - - name: drive-id - in: path - description: 'key: id of drive' - required: true - schema: - type: string - x-ms-docs-key-type: drive - - name: item-id - in: path - description: 'key: id of item' - required: true - schema: - type: string - x-ms-docs-key-type: item - requestBody: - description: In the request body, provide a JSON object with the following parameters. To create a custom role submit a list of actions instead of roles. - content: - application/json: - schema: - $ref: '#/components/schemas/driveItemInvite' - examples: - send viewer invite: - value: - recipients: - - '@libre.graph.recipient.type': user - objectId: "4c510ada-c86b-4815-8820-42cdf82c3d51" - roles: [ "b1e2218d-eef8-4d4c-b82d-0f1a1b48f3b5" ] - send viewer invite to multiple recipients: - value: - recipients: - - '@libre.graph.recipient.type': user - objectId: "4c510ada-c86b-4815-8820-42cdf82c3d51" - - '@libre.graph.recipient.type': user - objectId: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c" - roles: [ "b1e2218d-eef8-4d4c-b82d-0f1a1b48f3b5" ] - send editor invite with expiry: - value: - recipients: - - '@libre.graph.recipient.type': user - objectId: "4c510ada-c86b-4815-8820-42cdf82c3d51" - roles: [ "fb6c3e19-e378-47e5-b277-9732f9de6e21" ] - expirationDateTime: "2018-07-15T14:00:00.000Z" - send manager invite to group: - description: "Sharing with a groups requires setting the @libre.graph.recipient.type annotation." - value: - recipients: - - '@libre.graph.recipient.type': group - objectId: "167cbee2-0518-455a-bfb2-031fe0621e5d" - roles: [ "312c0871-5ef7-4b3a-85b6-0e4074c64049" ] - send invite to user granting custom actions: - description: "A Custom role is just a collection of actions, in this case allowing the user to stat and move driveItems" - value: - recipients: - - objectId: "4c510ada-c86b-4815-8820-42cdf82c3d51" - '@libre.graph.permissions.actions': [ "libre.graph/driveItem/basic/read", "libre.graph/driveItem/path/update" ] - responses: - '200': - description: Response - content: - application/json: - schema: - title: Collection of permissions - type: object - properties: - value: - type: array - items: - $ref: '#/components/schemas/permission' - examples: - send viewer invite: - value: - - id: "34646ab6-be32-43c9-89e6-987e0c237e9b" - roles: [ "b1e2218d-eef8-4d4c-b82d-0f1a1b48f3b5" ] - grantedToV2: - - user: - id: "4c510ada-c86b-4815-8820-42cdf82c3d51" - displayName: "Albert Einstein" - send viewer invite to multiple recipients: - description: multiple permissions are returned as each has received a dedicated sharing id - value: - - id: "81d5bad3-3eff-410a-a2ea-eda2d14d4474" - roles: [ "b1e2218d-eef8-4d4c-b82d-0f1a1b48f3b5" ] - grantedToV2: - - user: - id: "4c510ada-c86b-4815-8820-42cdf82c3d51" - displayName: "Albert Einstein" - - id: "b470677e-a7f5-4304-8ef5-f5056a21fff1" - roles: [ "b1e2218d-eef8-4d4c-b82d-0f1a1b48f3b5" ] - grantedToV2: - - user: - id: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c" - displayName: "Marie Skłodowska Curie" - send editor invite with expiry: - value: - - id: "453b02be-4ec2-4e7d-b576-09fc153de812" - roles: [ "fb6c3e19-e378-47e5-b277-9732f9de6e21" ] - grantedToV2: - - user: - id: "4c510ada-c86b-4815-8820-42cdf82c3d51" - displayName: "Albert Einstein" - expirationDateTime: "2018-07-15T14:00:00.000Z" - send manager invite to group: - value: - - id: "86765c0d-3905-444a-9b07-76201f8cf7df" - roles: [ "312c0871-5ef7-4b3a-85b6-0e4074c64049" ] - grantedToV2: - - group: - id: "167cbee2-0518-455a-bfb2-031fe0621e5d" - displayName: "Philosophy Haters" - send invite to user granting custom actions: - value: - - id: "c42b5cbd-2d65-42cf-b0b6-fb6d2b762256" - grantedToV2: - - user: - id: "4c510ada-c86b-4815-8820-42cdf82c3d51" - displayName: "Albert Einstein" - '@libre.graph.permissions.actions': [ "libre.graph/driveItem/basic/read", "libre.graph/driveItem/path/update" ] - '207': - # FIXME describe partial success response - description: Partial success response TODO - '400': - description: Bad request - content: - application/json: - schema: - $ref: '#/components/schemas/odata.error' - examples: - secureSharingInvalidRequest: - description: If the recipient was given via email, but does not belong to a known user account. - value: - - error: - - code: invalidRequest - message: Some users in the request cannot be invited securely. - innerError: - - code: secureSharingInvalidRequest - date: "2023-09-14T12:27:15" - request-id: b49f582d-9a0f-4731-ab0c-b9e50246ef58 - client-request-id: 832c7206-1a40-395c-e0d9-e85649b8b0b0 - default: - $ref: '#/components/responses/error' - # TODO add SendNotification errors as per https://learn.microsoft.com/en-us/graph/api/driveitem-invite?view=graph-rest-beta&tabs=http#sendnotification-errors - x-ms-docs-operation-type: operation - '/v1beta1/drives/{drive-id}/items/{item-id}/permissions': - get: - tags: - - drives.permissions - summary: List the effective sharing permissions on a driveItem. - operationId: ListPermissions - description: | - The permissions collection includes potentially sensitive information and may not be available for every caller. - - * For the owner of the item, all sharing permissions will be returned. This includes co-owners. - * For a non-owner caller, only the sharing permissions that apply to the caller are returned. - * Sharing permission properties that contain secrets (e.g. `webUrl`) are only returned for callers that are able to create the sharing permission. - - All permission objects have an `id`. A permission representing - * a link has the `link` facet filled with details. - * a share has the `roles` property set and the `grantedToV2` property filled with the grant recipient details. - - # TODO hm, the ms graph api seems to always fill the `roles` property to indicate the actions granted by the link. - # | Value | Display name | Description | role? | - # | -------------- | ----------------- | --------------------------------------------------------------- | -------------- | - # | view | View | Creates a read-only link to the driveItem. | viewer | - # | upload | Upload | Creates a read-write link to the folder driveItem. | uploader | - # | edit | Edit | Creates a read-write link to the driveItem. | editor | - # | createOnly | File Drop | Creates an upload-only link to the folder driveItem. | createOnly | - # | blocksDownload | Secure View | Creates a read-only link that blocks download to the driveItem. | blocksDownload | - # AFAICT we will have to define the actions granted by a link type when we start to implement this - # but e.g. a createOnly role and blocksDownload should not show up in the drop down for sharing files internally ... - # when we need to hardcode this anyway ... we should indicate that by using the buildIn flag ... but how do we prevent some roles from showing up in the internal sharing dialog ... - # for now leave out the roles property and hardcode the actions allowed by the link type - - parameters: - - name: drive-id - in: path - description: 'key: id of drive' - required: true - schema: - type: string - x-ms-docs-key-type: drive - - name: item-id - in: path - description: 'key: id of item' - required: true - schema: - type: string - x-ms-docs-key-type: item - - $ref: '#/components/parameters/permissionsFilter' - - $ref: '#/components/parameters/permissionsSelect' - - $ref: '#/components/parameters/count' - - $ref: '#/components/parameters/top' - responses: - '200': - description: Retrieved resource - content: - application/json: - schema: - title: Collection of permissions with allowed values - type: object - properties: - '@libre.graph.permissions.roles.allowedValues': - type: array - description: A list of role definitions that can be chosen for the resource. - items: - $ref: '#/components/schemas/unifiedRoleDefinition' - '@libre.graph.permissions.actions.allowedValues': - type: array - description: | - A list of actions that can be chosen for a custom role. - - Following the CS3 API we can represent the CS3 permissions by mapping them to driveItem properties or relations like this: - | [CS3 ResourcePermission](https://cs3org.github.io/cs3apis/#cs3.storage.provider.v1beta1.ResourcePermissions) | action | comment | - | ------------------------------------------------------------------------------------------------------------ | ------ | ------- | - | `stat` | `libre.graph/driveItem/basic/read` | `basic` because it does not include versions or trashed items | - | `get_quota` | `libre.graph/driveItem/quota/read` | read only the `quota` property | - | `get_path` | `libre.graph/driveItem/path/read` | read only the `path` property | - | `move` | `libre.graph/driveItem/path/update` | allows updating the `path` property of a CS3 resource | - | `delete` | `libre.graph/driveItem/standard/delete` | `standard` because deleting is a common update operation | - | `list_container` | `libre.graph/driveItem/children/read` | | - | `create_container` | `libre.graph/driveItem/children/create` | | - | `initiate_file_download` | `libre.graph/driveItem/content/read` | `content` is the property read when initiating a download | - | `initiate_file_upload` | `libre.graph/driveItem/upload/create` | `uploads` are a separate property. postprocessing creates the `content` | - | `add_grant` | `libre.graph/driveItem/permissions/create` | | - | `list_grant` | `libre.graph/driveItem/permissions/read` | | - | `update_grant` | `libre.graph/driveItem/permissions/update` | | - | `remove_grant` | `libre.graph/driveItem/permissions/delete` | | - | `deny_grant` | `libre.graph/driveItem/permissions/deny` | uses a non CRUD action `deny` | - | `list_file_versions` | `libre.graph/driveItem/versions/read` | `versions` is a `driveItemVersion` collection | - | `restore_file_version` | `libre.graph/driveItem/versions/update` | the only `update` action is restore | - | `list_recycle` | `libre.graph/driveItem/deleted/read` | reading a driveItem `deleted` property implies listing | - | `restore_recycle_item` | `libre.graph/driveItem/deleted/update` | the only `update` action is restore | - | `purge_recycle` | `libre.graph/driveItem/deleted/delete` | allows purging deleted `driveItems` | - items: - type: string - value: - type: array - items: - $ref: '#/components/schemas/permission' - # maxItems: 100 - '@odata.count': - type: integer - description: The total number of permissions available, only present if the `count` query parameter is set to true. - #'@odata.nextLink': - # type: string - example: - '@libre.graph.permissions.roles.allowedValues': - - id: b1e2218d-eef8-4d4c-b82d-0f1a1b48f3b5 - description: "Allows reading the shared file or folder" - displayName: Viewer - # we don't want to reproduce all the individual role permissions, that is what the /roleManagement/permissions/roleDefinitions endpoint is for - # to build a custom role - #rolePermissions: - # - allowedResourceActions: - # - libre.graph/driveItem/basic/read - # - libre.graph/driveItem/permissions/read - # - libre.graph/driveItem/upload/create - # condition: "exists @Resource.File" - '@libre.graph.weight': 1 - - id: fb6c3e19-e378-47e5-b277-9732f9de6e21 - description: "Allows reading and writing the shared file or folder" - displayName: Editor - #rolePermissions: - # - allowedResourceActions: - # - libre.graph/driveItem/standard/allTasks - # condition: "exists @Resource.File" - '@libre.graph.weight': 2 - - id: 312c0871-5ef7-4b3a-85b6-0e4074c64049 - description: "Allows managing a space" - displayName: Manager - #rolePermissions: - # - allowedResourceActions: - # - libre.graph/drive/standard/allTasks - # condition: "exists @Resource.File" - '@libre.graph.weight': 3 - - id: 4916f47e-66d5-49bb-9ac9-748ad00334b - description: "Allows creating new files" - displayName: File Drop - #rolePermissions: - # - allowedResourceActions: - # libre.graph/driveItem/upload/create - # condition: "exists @Resource.File" - '@libre.graph.weight': 4 - '@libre.graph.permissions.actions.allowedValues': [ - "libre.graph/driveItem/basic/read", - "libre.graph/driveItem/permissions/read", - "libre.graph/driveItem/upload/create", - "libre.graph/driveItem/standard/allTasks", - "libre.graph/driveItem/upload/create" - ] - - value: - - id: "67445fde-a647-4dd4-b015-fc5dafd2821d" - link: - type: "view" - webUrl: "https://cloud.example.org/s/fhGBMIkKFEHWysj" - - - id: "34646ab6-be32-43c9-89e6-987e0c237e9b" - roles: [ "b1e2218d-eef8-4d4c-b82d-0f1a1b48f3b5" ] - grantedToV2: - - user: - id: "4c510ada-c86b-4815-8820-42cdf82c3d51" - displayName: "Albert Einstein" - - id: "81d5bad3-3eff-410a-a2ea-eda2d14d4474" - roles: [ "b1e2218d-eef8-4d4c-b82d-0f1a1b48f3b5" ] - grantedToV2: - - user: - id: "4c510ada-c86b-4815-8820-42cdf82c3d51" - displayName: "Albert Einstein" - - id: "b470677e-a7f5-4304-8ef5-f5056a21fff1" - roles: [ "b1e2218d-eef8-4d4c-b82d-0f1a1b48f3b5" ] - grantedToV2: - - user: - id: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c" - displayName: "Marie Skłodowska Curie" - - id: "453b02be-4ec2-4e7d-b576-09fc153de812" - roles: [ "fb6c3e19-e378-47e5-b277-9732f9de6e21" ] - grantedToV2: - - user: - id: "4c510ada-c86b-4815-8820-42cdf82c3d51" - displayName: "Albert Einstein" - expirationDateTime: "2018-07-15T14:00:00.000Z" - - id: "86765c0d-3905-444a-9b07-76201f8cf7df" - roles: [ "312c0871-5ef7-4b3a-85b6-0e4074c64049" ] - grantedToV2: - - group: - id: "167cbee2-0518-455a-bfb2-031fe0621e5d" - displayName: "Philosophy Haters" - - id: "c42b5cbd-2d65-42cf-b0b6-fb6d2b762256" - grantedToV2: - - user: - id: "4c510ada-c86b-4815-8820-42cdf82c3d51" - displayName: "Albert Einstein" - '@libre.graph.permissions.actions': [ "libre.graph/driveItem/basic/read", "libre.graph/driveItem/path/update" ] - default: - $ref: '#/components/responses/error' - x-ms-docs-operation-type: operation - '/v1beta1/drives/{drive-id}/items/{item-id}/permissions/{perm-id}': - get: - tags: - - drives.permissions - summary: Get sharing permission for a file or folder - operationId: GetPermission - description: | - Return the effective sharing permission for a particular permission resource. - - # TODO do we need to implement inherited permissions from the start? - # Effective permissions of an item can come from two sources: permissions set directly on the item itself or permissions that are inherited from the item's ancestors. - # - # Callers can differentiate if the permission is inherited or not by checking the `inheritedFrom` property. This property is an ItemReference resource referencing the ancestor that the permission is inherited from. - # - parameters: - - name: drive-id - in: path - description: 'key: id of drive' - required: true - schema: - type: string - x-ms-docs-key-type: drive - - name: item-id - in: path - description: 'key: id of item' - required: true - schema: - type: string - x-ms-docs-key-type: item - - name: perm-id - in: path - description: 'key: id of permission' - required: true - schema: - type: string - x-ms-docs-key-type: permission - responses: - '200': - description: Retrieved resource - content: - application/json: - schema: - $ref: '#/components/schemas/permission' - examples: - edit permission: - value: - id: "81d5bad3-3eff-410a-a2ea-eda2d14d4474" - roles: [ "write" ] - grantedToV2: - - user: - id: "4c510ada-c86b-4815-8820-42cdf82c3d51" - displayName: "Albert Einstein" - default: - $ref: '#/components/responses/error' - x-ms-docs-operation-type: operation - patch: - tags: - - drives.permissions - summary: Update sharing permission - operationId: UpdatePermission - description: | - Update the properties of a sharing permission by patching the permission resource. - - Only the `roles`, `expirationDateTime` and `password` properties can be modified this way. - parameters: - - name: drive-id - in: path - description: 'key: id of drive' - required: true - schema: - type: string - x-ms-docs-key-type: drive - - name: item-id - in: path - description: 'key: id of item' - required: true - schema: - type: string - x-ms-docs-key-type: item - - name: perm-id - in: path - description: 'key: id of permission' - required: true - schema: - type: string - x-ms-docs-key-type: permission - #- name: If-Match - # in: header - # description: | - # If this request header is included and the eTag (or cTag) provided does not match the current tag on the item, - # a `412 Precondition Failed`` response is returned and the item will not be updated. - # schema: - # type: string - requestBody: - description: New property values - content: - application/json: - schema: - $ref: '#/components/schemas/permission' - examples: - update link: - description: Updating a public link is done by setting the `type` property of the permission link. - value: - link: - type: edit - update invite: - description: Updating an invite is done by setting the `roles` property of the permission. - value: - roles: [ "b1e2218d-eef8-4d4c-b82d-0f1a1b48f3b5" ] - required: true - responses: - '200': - description: Updated permission - content: - application/json: - schema: - $ref: '#/components/schemas/permission' - examples: - update link: - value: - id: "a373c812-d786-42ae-a65f-ed87ee4abd49" - link: - type: "edit" - webUrl: "https://cloud.example.org/s/MlnTDkhcBlihNzO" - update invite: - value: - id: "81d5bad3-3eff-410a-a2ea-eda2d14d4474" - roles: [ "b1e2218d-eef8-4d4c-b82d-0f1a1b48f3b5" ] - expirationDateTime: "2018-07-15T14:00:00.000Z" - grantedToV2: - - user: - id: "4c510ada-c86b-4815-8820-42cdf82c3d51" - displayName: "Albert Einstein" - default: - $ref: '#/components/responses/error' - x-ms-docs-operation-type: operation - delete: - tags: - - drives.permissions - summary: Remove access to a DriveItem - operationId: DeletePermission - description: | - Remove access to a DriveItem. - - Only sharing permissions that are not inherited can be deleted. The `inheritedFrom` property must be `null`. - parameters: - - name: drive-id - in: path - description: 'key: id of drive' - required: true - schema: - type: string - x-ms-docs-key-type: drive - - name: item-id - in: path - description: 'key: id of item' - required: true - schema: - type: string - x-ms-docs-key-type: item - - name: perm-id - in: path - description: 'key: id of permission' - required: true - schema: - type: string - x-ms-docs-key-type: permission - #- name: If-Match - # in: header - # description: | - # If this request header is included and the eTag (or cTag) provided does not match the current tag on the item, - # a `412 Precondition Failed`` response is returned and the item will not be updated. - # schema: - # type: string - responses: - '204': - description: Success - default: - $ref: '#/components/responses/error' - x-ms-docs-operation-type: operation - '/v1beta1/drives/{drive-id}/items/{item-id}/permissions/{perm-id}/setPassword': - post: - tags: - - drives.permissions - summary: Set sharing link password - operationId: SetPermissionPassword - description: | - Set the password of a sharing permission. - - Only the `password` property can be modified this way. - parameters: - - name: drive-id - in: path - description: 'key: id of drive' - required: true - schema: - type: string - x-ms-docs-key-type: drive - - name: item-id - in: path - description: 'key: id of item' - required: true - schema: - type: string - x-ms-docs-key-type: item - - name: perm-id - in: path - description: 'key: id of permission' - required: true - schema: - type: string - x-ms-docs-key-type: permission - #- name: If-Match - # in: header - # description: | - # If this request header is included and the eTag (or cTag) provided does not match the current tag on the item, - # a `412 Precondition Failed`` response is returned and the item will not be updated. - # schema: - # type: string - requestBody: - description: New password value - content: - application/json: - schema: - $ref: '#/components/schemas/sharingLinkPassword' - examples: - set password: - value: - password: "TestPassword123!" - set empty password: - value: - password: "" - required: true - responses: - '200': - description: Updated permission - content: - application/json: - schema: - $ref: '#/components/schemas/permission' - examples: - set password on editor link with custom display name: - value: - - id: "7181275c-557e-4adf-8e44-abdcf5389b6a" - hasPassword: true - expirationDateTime: "2018-07-15T14:00:00.000Z" - link: - type: "edit" - displayName: "Homework" - webUrl: "https://cloud.example.org/s/fdleMyGeerkIZTJ" - set empty password: - value: - - id: "765c9b7b-6ccf-46ff-a659-95501250229c" - hasPassword: false - link: - type: "view" - displayName: "Quick link" - quicklink: true - webUrl: "https://cloud.example.org/s/erjHgjKjkstTesG" - default: - $ref: '#/components/responses/error' - x-ms-docs-operation-type: operation - '/v1.0/drives/{drive-id}/root': - get: - tags: - - drives.root - summary: Get root from arbitrary space - operationId: GetRoot - parameters: - - name: drive-id - in: path - description: 'key: id of drive' - required: true - schema: - type: string - x-ms-docs-key-type: drive - responses: - '200': - description: Retrieved resource - content: - application/json: - schema: - $ref: '#/components/schemas/driveItem' - default: - $ref: '#/components/responses/error' - x-ms-docs-operation-type: operation - /v1.0/groups: - get: - tags: - - groups - summary: Get entities from groups - operationId: ListGroups - parameters: - - $ref: '#/components/parameters/search' - - name: $orderby - in: query - description: Order items by property values - style: form - explode: false - schema: - uniqueItems: true - type: array - items: - enum: - - displayName - - displayName desc - type: string - - name: $select - in: query - description: Select properties to be returned - style: form - explode: false - schema: - uniqueItems: true - type: array - items: - enum: - - id - - description - - displayName - - mail - - members - type: string - - name: $expand - in: query - description: Expand related entities - style: form - explode: false - schema: - uniqueItems: true - type: array - items: - enum: - - members - type: string - responses: - '200': - description: Retrieved entities - content: - application/json: - schema: - title: Collection of group - type: object - properties: - value: - type: array - items: - $ref: '#/components/schemas/group' - '@odata.nextLink': - type: string - default: - $ref: '#/components/responses/error' - x-ms-pageable: - nextLinkName: '@odata.nextLink' - operationName: listMore - x-ms-docs-operation-type: operation - post: - tags: - - groups - summary: Add new entity to groups - operationId: CreateGroup - requestBody: - description: New entity - content: - application/json: - schema: - $ref: '#/components/schemas/group' - required: true - responses: - '201': - description: Created entity - content: - application/json: - schema: - $ref: '#/components/schemas/group' - default: - $ref: '#/components/responses/error' - x-ms-docs-operation-type: operation - '/v1.0/groups/{group-id}': - get: - tags: - - group - summary: Get entity from groups by key - operationId: GetGroup - parameters: - - name: group-id - in: path - description: 'key: id or name of group' - required: true - schema: - type: string - x-ms-docs-key-type: group - - name: $select - in: query - description: Select properties to be returned - style: form - explode: false - schema: - uniqueItems: true - type: array - items: - enum: - - id - - description - - displayName - - members - type: string - - name: $expand - in: query - description: Expand related entities - style: form - explode: false - schema: - uniqueItems: true - type: array - items: - enum: - - members - type: string - responses: - '200': - description: Retrieved entity - content: - application/json: - schema: - $ref: '#/components/schemas/group' - default: - $ref: '#/components/responses/error' - x-ms-docs-operation-type: operation - patch: - tags: - - group - summary: Update entity in groups - operationId: UpdateGroup - parameters: - - name: group-id - in: path - description: 'key: id of group' - required: true - schema: - type: string - x-ms-docs-key-type: group - requestBody: - description: New property values - content: - application/json: - schema: - $ref: '#/components/schemas/group' - examples: - update name: - value: - displayName: GroupName - add multiple members: - value: - '@members@odata.bind': - - "https://localhost:9200/graph/v1.0/users/4c510ada-c86b-4815-8820-42cdf82c3d51" - - "https://localhost:9200/graph/v1.0/users/c54b0588-7157-4521-bb52-c1c8ca84ea71" - required: true - responses: - '204': - description: Success - default: - $ref: '#/components/responses/error' - x-ms-docs-operation-type: operation - delete: - tags: - - group - summary: Delete entity from groups - operationId: DeleteGroup - parameters: - - name: group-id - in: path - description: 'key: id of group' - required: true - schema: - type: string - x-ms-docs-key-type: group - - name: If-Match - in: header - description: ETag - schema: - type: string - responses: - '204': - description: Success - default: - $ref: '#/components/responses/error' - x-ms-docs-operation-type: operation - '/v1.0/groups/{group-id}/members': - get: - tags: - - group - summary: Get a list of the group's direct members - operationId: ListMembers - parameters: - - name: group-id - in: path - description: 'key: id or name of group' - required: true - schema: - type: string - example: 86948e45-96a6-43df-b83d-46e92afd30de - responses: - '200': - description: Retrieved group members - content: - application/json: - schema: - title: Collection of users - type: object - properties: - value: - type: array - items: - $ref: '#/components/schemas/user' - example: - - id: 90eedea1-dea1-90ee-a1de-ee90a1deee90 - onPremisesSAMAccountName: max.mustermann - displayName: Max Mustermann - mail: max.musterman@example.org - default: - $ref: '#/components/responses/error' - '/v1.0/groups/{group-id}/members/$ref': - post: - tags: - - group - summary: Add a member to a group - operationId: AddMember - parameters: - - name: group-id - in: path - description: 'key: id of group' - required: true - schema: - type: string - x-ms-docs-key-type: group - requestBody: - description: Object to be added as member - content: - application/json: - schema: - title: Member Reference - type: object - properties: - '@odata.id': - type: string - required: true - responses: - '204': - description: Success - default: - $ref: '#/components/responses/error' - x-ms-docs-operation-type: operation - '/v1.0/groups/{group-id}/members/{directory-object-id}/$ref': - delete: - tags: - - group - summary: Delete member from a group - operationId: DeleteMember - parameters: - - name: group-id - in: path - description: 'key: id of group' - required: true - schema: - type: string - x-ms-docs-key-type: group - - name: directory-object-id - in: path - description: 'key: id of group member to remove' - required: true - schema: - type: string - - name: If-Match - in: header - description: ETag - schema: - type: string - responses: - '204': - description: Success - default: - $ref: '#/components/responses/error' - x-ms-docs-operation-type: operation - /v1.0/me/drive: - get: - tags: - - me.drive - summary: Get personal space for user - operationId: GetHome - responses: - '200': - description: Retrieved personal space - content: - application/json: - schema: - $ref: '#/components/schemas/drive' - default: - $ref: '#/components/responses/error' - x-ms-docs-operation-type: operation - /v1.0/me/drive/root: - get: - tags: - - me.drive.root - summary: Get root from personal space - operationId: HomeGetRoot - responses: - '200': - description: Retrieved resource - content: - application/json: - schema: - $ref: '#/components/schemas/driveItem' - default: - $ref: '#/components/responses/error' - x-ms-docs-operation-type: operation - /v1.0/me/drive/root/children: - get: - tags: - - me.drive.root.children - summary: Get children from drive - operationId: HomeGetChildren - responses: - '200': - description: Retrieved resource list - content: - application/json: - schema: - title: Collection of driveItems - type: object - properties: - value: - type: array - items: - $ref: '#/components/schemas/driveItem' - maxItems: 100 - '@odata.nextLink': - type: string - default: - $ref: '#/components/responses/error' - x-ms-docs-operation-type: operation - /v1beta1/me/drive/sharedByMe: - get: - tags: - - me.drive - summary: Get a list of driveItem objects shared by the current user. - operationId: ListSharedByMe - description: | - The `driveItems` returned from the `sharedByMe` method always include the `permissions` relation that indicates they are shared items. - parameters: - - name: $expand - in: query - description: Expand related entities - style: form - explode: false - schema: - uniqueItems: true - type: array - items: - enum: - - thumbnails - type: string - responses: - '200': - description: OK - content: - application/json: - schema: - title: Collection of driveItems - type: object - properties: - value: - type: array - items: - $ref: '#/components/schemas/driveItem' - # maxItems: 20 - #'@odata.nextLink': - # type: string - example: - value: - - id: 78363031-03ef-4eda-84a2-243a691a13cd - createdDateTime: "2020-02-19T14:23:25.52Z" - # not yet supported by opencloud - #cTag: "adDpDMTI2NDRBMTRCMEE3NzUwITEzNzkuNjM3NjYyNzQ5NjU1MDMwMDAw" - eTag: "aQzEyNjQ0QTE0QjBBNzc1MCExMzc5LjQ" - lastModifiedDateTime: "2021-09-03T14:09:25.503Z" - name: "March Proposal.docx" - parentReference: - driveId: "1991210caf" - driveType: personal - permissions: - - id: "81d5bad3-3eff-410a-a2ea-eda2d14d4474" - roles: [ "write" ] - grantedToV2: - - user: - id: "4c510ada-c86b-4815-8820-42cdf82c3d51" - displayName: "Albert Einstein" - - id: "81d5bad3-3eff-410a-a2ea-eda2d14d4474" - roles: [ "read" ] - grantedToV2: - - user: - id: "48016357-346a-443e-bf7a-945c9448a99b" - displayName: "Marie Curie" - default: - $ref: '#/components/responses/error' - x-ms-docs-operation-type: operation - /v1.0/me/drive/items/{item-id}/follow: - post: - tags: - - me.drive - summary: Follow a DriveItem - description: Follow a DriveItem. - operationId: FollowDriveItem - parameters: - - name: item-id - in: path - description: 'key: id of item' - required: true - schema: - type: string - x-ms-docs-key-type: item - responses: - "201": - description: Created - content: - application/json: - schema: - $ref: "#/components/schemas/driveItem" - default: - $ref: "#/components/responses/error" - x-ms-docs-operation-type: operation - /v1.0/me/drive/following/{item-id}: - delete: - tags: - - me.drive - summary: Unfollow a DriveItem - description: Unfollow a DriveItem. - operationId: UnfollowDriveItem - parameters: - - name: item-id - in: path - description: "key: id of item" - required: true - schema: - type: string - x-ms-docs-key-type: item - responses: - "204": - description: No Content - default: - $ref: "#/components/responses/error" - x-ms-docs-operation-type: operation - - /v1beta1/me/drive/sharedWithMe: - get: - tags: - - me.drive - summary: Get a list of driveItem objects shared with the owner of a drive. - operationId: ListSharedWithMe - description: | - The `driveItems` returned from the `sharedWithMe` method always include the `remoteItem` facet that indicates they are items from a different drive. - parameters: - - name: $expand - in: query - description: Expand related entities - style: form - explode: false - schema: - uniqueItems: true - type: array - items: - enum: - - thumbnails - type: string - responses: - '200': - description: OK - content: - application/json: - schema: - title: Collection of driveItems - type: object - properties: - value: - type: array - items: - $ref: '#/components/schemas/driveItem' - # maxItems: 20 - #'@odata.nextLink': - # type: string - example: - value: - - id: 78363031-03ef-4eda-84a2-243a691a13cd - '@UI.hidden': false - '@client.synchronize': true - createdBy: - user: - displayName: "Albert Einstein" - id: "44feab10d55e9871" - cTag: "adDpDMTI2NDRBMTRCMEE3NzUwITEzNzkuNjM3NjYyNzQ5NjU1MDMwMDAw" - eTag: "aQzEyNjQ0QTE0QjBBNzc1MCExMzc5LjQ" - lastModifiedDateTime: "2021-09-03T14:09:25.503Z" - name: "March Proposal.docx" - parentReference: - driveId: "1991210caf" - driveType: personal - remoteItem: - id: 02d7a7df-a6f7-44cc-848a-3c98f7dd5046 - name: "March Proposal.docx" - size: 19121 - lastModifiedDateTime: "2021-09-03T14:09:25.503Z" - permissions: - - id: 7310e99f-377b-4cba-a1f0-ffa3331fd32c - roles: - - b1e2218d-eef8-4d4c-b82d-0f1a1b48f3b5 - grantedToV2: - group: - displayName: "users" - id: 509a9dcd-bb37-4f4f-a01a-19dca27d9cfa - invitation: - invitedBy: - user: - displayName: "Albert Einstein" - id: "44feab10d55e9871" - default: - $ref: '#/components/responses/error' - x-ms-docs-operation-type: operation - - /v1.0/invitations: - post: - tags: - - invitations - summary: Create a new invitation - operationId: CreateInvitation - requestBody: - description: New invitation - content: - application/json: - schema: - $ref: '#/components/schemas/invitation' - responses: - '201': - description: Invitation created successfully - content: - application/json: - schema: - $ref: '#/components/schemas/invitation' - x-ms-docs-operation-type: operation - get: - tags: - - invitations - summary: Get a list of invitations - operationId: ListInvitations - responses: - '200': - description: Retrieved invitations - content: - application/json: - schema: - title: Collection of invitations - type: object - properties: - value: - type: array - items: - $ref: '#/components/schemas/invitation' - - /v1.0/invitations/{invitation-id}: - get: - tags: - - invitations - summary: Get an invitation by key - operationId: GetInvitation - parameters: - - name: invitation-id - in: path - description: 'key: id of invitation' - required: true - schema: - type: string - responses: - '200': - description: Retrieved invitation - content: - application/json: - schema: - $ref: '#/components/schemas/invitation' - - /v1.0/users: - get: - tags: - - users - summary: Get entities from users - operationId: ListUsers - parameters: - - $ref: '#/components/parameters/search' - - $ref: '#/components/parameters/usersFilter' - - name: $orderby - in: query - description: Order items by property values - style: form - explode: false - schema: - uniqueItems: true - type: array - items: - enum: - - displayName - - displayName desc - - mail - - mail desc - - onPremisesSamAccountName - - onPremisesSamAccountName desc - type: string - - name: $select - in: query - description: Select properties to be returned - style: form - explode: false - schema: - uniqueItems: true - type: array - items: - enum: - - id - - displayName - - mail - - memberOf - - onPremisesSamAccountName - - surname - type: string - - name: $expand - in: query - description: Expand related entities - style: form - explode: false - schema: - uniqueItems: true - type: array - items: - enum: - - drive - - drives - - memberOf - - appRoleAssignments - type: string - responses: - '200': - description: Retrieved entities - content: - application/json: - schema: - title: Collection of user - type: object - properties: - value: - type: array - items: - $ref: '#/components/schemas/user' - '@odata.nextLink': - type: string - default: - $ref: '#/components/responses/error' - x-ms-pageable: - nextLinkName: '@odata.nextLink' - operationName: listMore - x-ms-docs-operation-type: operation - post: - tags: - - users - summary: Add new entity to users - operationId: CreateUser - requestBody: - description: New entity - content: - application/json: - schema: - $ref: '#/components/schemas/user' - required: true - responses: - '201': - description: Created entity - content: - application/json: - schema: - $ref: '#/components/schemas/user' - default: - $ref: '#/components/responses/error' - x-ms-docs-operation-type: operation - '/v1.0/users/{user-id}': - get: - tags: - - user - summary: Get entity from users by key - operationId: GetUser - parameters: - - name: user-id - in: path - description: 'key: id or name of user' - required: true - schema: - type: string - x-ms-docs-key-type: user - - name: $select - in: query - description: Select properties to be returned - style: form - explode: false - schema: - uniqueItems: true - type: array - items: - enum: - - id - - displayName - - drive - - drives - - mail - - memberOf - - onPremisesSamAccountName - - surname - type: string - - name: $expand - in: query - description: Expand related entities - style: form - explode: false - schema: - uniqueItems: true - type: array - items: - enum: - - drive - - drives - - memberOf - - appRoleAssignments - type: string - responses: - '200': - description: Retrieved entity - content: - application/json: - schema: - $ref: '#/components/schemas/user' - default: - $ref: '#/components/responses/error' - x-ms-docs-operation-type: operation - patch: - tags: - - user - summary: Update entity in users - operationId: UpdateUser - parameters: - - name: user-id - in: path - description: 'key: id of user' - required: true - schema: - type: string - x-ms-docs-key-type: user - requestBody: - description: New property values - content: - application/json: - schema: - $ref: '#/components/schemas/userUpdate' - examples: - change displayName: - value: - displayName: "Marie Skłodowska Curie" - change email: - value: - mail: "marie@example.com" - change preferred language: - value: - preferredLanguage: "fr" - required: true - responses: - '200': - description: Success - content: - application/json: - schema: - $ref: '#/components/schemas/user' - default: - $ref: '#/components/responses/error' - x-ms-docs-operation-type: operation - delete: - tags: - - user - summary: Delete entity from users - operationId: DeleteUser - parameters: - - name: user-id - in: path - description: 'key: id or name of user' - required: true - schema: - type: string - x-ms-docs-key-type: user - - name: If-Match - in: header - description: ETag - schema: - type: string - responses: - '204': - description: Success - default: - $ref: '#/components/responses/error' - x-ms-docs-operation-type: operation - '/v1.0/users/{user-id}/exportPersonalData': - post: - tags: - - user - summary: export personal data of a user - operationId: ExportPersonalData - parameters: - - name: user-id - in: path - description: 'key: id or name of user' - required: true - schema: - type: string - x-ms-docs-key-type: user - requestBody: - description: destination the file should be created at - content: - application/json: - schema: - properties: - storageLocation: - type: string - description: the path where the file should be created in the users personal space - responses: - 202: - description: success - default: - $ref: '#/components/responses/error' - x-ms-docs-operation-type: operation - '/v1.0/users/{user-id}/appRoleAssignments': - description: Provides operations to manage assignments of roles to users. - get: - tags: - - user.appRoleAssignment - summary: Get appRoleAssignments from a user - description: Represents the global roles a user has been granted for an application. - operationId: user.ListAppRoleAssignments - responses: - 200: - description: Retrieved appRoleAssignments - content: - application/json: - schema: - title: Collection of appRoleAssignments - type: object - properties: - value: - type: array - items: - $ref: '#/components/schemas/appRoleAssignment' - maxItems: 100 - '@odata.nextLink': - type: string - default: - $ref: '#/components/responses/error' - post: - tags: - - user.appRoleAssignment - summary: Grant an appRoleAssignment to a user - description: | - Use this API to assign a global role to a user. To grant an app role assignment to a user, you need three identifiers: - * `principalId`: The `id` of the user to whom you are assigning the app role. - * `resourceId`: The `id` of the resource `servicePrincipal` or `application` that has defined the app role. - * `appRoleId`: The `id` of the `appRole` (defined on the resource service principal or application) to assign to the user. - operationId: user.CreateAppRoleAssignments - requestBody: - description: New app role assignment value - content: - application/json: - schema: - $ref: '#/components/schemas/appRoleAssignment' - required: true - responses: - 200: - description: Created new app role assignment. - content: - application/json: - schema: - $ref: '#/components/schemas/appRoleAssignment' - default: - $ref: '#/components/responses/error' - parameters: - - name: user-id - in: path - description: 'key: id of user' - required: true - style: simple - schema: - type: string - '/v1.0/users/{user-id}/appRoleAssignments/{appRoleAssignment-id}': - description: Provides operations to manage a global role for an assigned user. - delete: - tags: - - user.appRoleAssignment - summary: Delete the appRoleAssignment from a user - operationId: user.DeleteAppRoleAssignments - parameters: - - name: If-Match - in: header - description: ETag - style: simple - schema: - type: string - responses: - '204': - description: Success - default: - $ref: '#/components/responses/error' - parameters: - - name: user-id - in: path - description: 'key: id of user' - required: true - style: simple - schema: - type: string - - name: appRoleAssignment-id - in: path - description: 'key: id of appRoleAssignment. This is the concatenated {user-id}:{appRole-id} separated by a colon.' - required: true - style: simple - schema: - type: string - '/v1.0/users/{user-id}/photo/$value': - get: - tags: - - user.photo - summary: Get the photo of a user - operationId: GetUserPhoto - parameters: - - name: user-id - in: path - description: 'key: id or name of user' - required: true - schema: - type: string - x-ms-docs-key-type: user - responses: - '200': - description: Retrieved photo - content: - image/jpeg: - schema: - type: string - format: binary - default: - $ref: '#/components/responses/error' - /v1.0/education/users: - get: - tags: - - educationUser - summary: Get entities from education users - description: | - Retrieves a collection of education users with optional filtering, ordering, and expansion. - - **Filtering by external ID:** - Use `$filter` to query users by their external identifier, for example: - `$filter=externalId eq 'EX12345'` - security: - - bearerAuth: [ ] - operationId: ListEducationUsers - parameters: - - name: $filter - in: query - description: | - Filter items by property values. Supports a subset of OData filter expressions. - - **Supported filters:** - - By external ID: `externalId eq 'ext_12345'` - schema: - type: string - examples: - externalId: - value: externalId eq 'ext_12345' - summary: Filter by external ID - - name: $orderby - in: query - description: Order items by property values - style: form - explode: false - schema: - uniqueItems: true - type: array - items: - enum: - - displayName - - displayName desc - - mail - - mail desc - - onPremisesSamAccountName - - onPremisesSamAccountName desc - type: string - - name: $expand - in: query - description: Expand related entities - style: form - explode: false - schema: - uniqueItems: true - type: array - items: - enum: - - memberOf - type: string - responses: - '200': - description: Retrieved entities - content: - application/json: - schema: - title: Collection of educationUser - type: object - properties: - value: - type: array - items: - $ref: '#/components/schemas/educationUser' - example: - - id: 90eedea1-dea1-90ee-a1de-ee90a1deee90 - onPremisesSAMAccountName: max.mustermann - displayName: Max Mustermann - mail: max.musterman@example.org - primaryRole: student - identities: - - issuer: idp.school.com - issuerAssignedId: max.mustermann - - issuer: newidp.school.com - issuerAssignedId: ieZ2AeV5 - default: - $ref: '#/components/responses/error' - post: - tags: - - educationUser - summary: Add new education user - security: - - bearerAuth: [ ] - operationId: CreateEducationUser - requestBody: - description: New entity - content: - application/json: - schema: - $ref: '#/components/schemas/educationUser' - examples: - student: - $ref: '#/components/examples/educationUser_post' - externalId: - $ref: '#/components/examples/educationUser_post_externalId' - required: true - responses: - '201': - description: Created entity - content: - application/json: - schema: - $ref: '#/components/schemas/educationUser' - examples: - student: - $ref: '#/components/examples/educationUser' - default: - $ref: '#/components/responses/error' - '/v1.0/education/users/{user-id}': - get: - tags: - - educationUser - summary: Get properties of educationUser - security: - - bearerAuth: [ ] - operationId: GetEducationUser - parameters: - - name: user-id - in: path - description: 'key: id or username of user' - required: true - schema: - type: string - examples: - id: - value: 90eedea1-dea1-90ee-a1de-ee90a1deee90 - name: - value: max.mustermann - - name: $expand - in: query - description: Expand related entities - style: form - explode: false - schema: - uniqueItems: true - type: array - items: - enum: - - memberOf - type: string - responses: - '200': - description: Retrieved entity - content: - application/json: - schema: - $ref: '#/components/schemas/educationUser' - examples: - student: - $ref: '#/components/examples/educationUser' - default: - $ref: '#/components/responses/error' - patch: - tags: - - educationUser - summary: Update properties of educationUser - security: - - bearerAuth: [ ] - operationId: UpdateEducationUser - parameters: - - name: user-id - in: path - description: 'key: id or username of user' - required: true - schema: - type: string - example: 90eedea1-dea1-90ee-a1de-ee90a1deee90 - requestBody: - description: New property values - content: - application/json: - schema: - $ref: '#/components/schemas/educationUser' - example: - mail: max.mustermann@new.domain - required: true - responses: - '200': - description: Success - content: - application/json: - schema: - $ref: '#/components/schemas/educationUser' - example: - id: 90eedea1-dea1-90ee-a1de-ee90a1deee90 - onPremisesSAMAccountName: max.mustermann - displayName: Max Mustermann - surname: Mustermann - givenName: Max - mail: max.musterman@new.domain - primaryRole: student - identities: - - issuer: idp.school.com - issuerAssignedId: max.mustermann - '204': - description: Success - default: - $ref: '#/components/responses/error' - delete: - tags: - - educationUser - summary: Delete educationUser - description: | - Deletes an education user by their internal ID. - - **To delete by external ID:** - If you only have an external ID, you must first retrieve the user's internal ID: - 1. Call `GET /graph/v1.0/education/users?$filter=externalId eq '{value}'` - 2. Extract the `id` from the response - 3. Use that `id` in this DELETE endpoint - - See the [ListEducationUsers](#/educationUser/ListEducationUsers) operation for query details. - security: - - bearerAuth: [ ] - operationId: DeleteEducationUser - parameters: - - name: user-id - in: path - description: | - key: internal user id (UUID format) or username of user. - - **Note:** If you only have an external ID, first query the user - with `GET /graph/v1.0/education/users?$filter=externalId eq '{value}'` - to retrieve the internal ID. - required: true - schema: - type: string - example: 90eedea1-dea1-90ee-a1de-ee90a1deee90 - responses: - '204': - description: Success - '404': - description: User not found - default: - $ref: '#/components/responses/error' - /v1.0/education/schools: - get: - tags: - - educationSchool - summary: Get a list of schools and their properties - description: | - Retrieves a collection of education schools with optional filtering and ordering. - - **Filtering by external ID:** - Use `$filter` to query schools by their external identifier, for example: - `$filter=externalId eq 'EX12345'` - security: - - bearerAuth: [ ] - operationId: ListSchools - parameters: - - name: $filter - in: query - description: | - Filter items by property values. Supports a subset of OData filter expressions. - - **Supported filters:** - - By external ID: `externalId eq 'ext_12345'` - schema: - type: string - examples: - externalId: - value: externalId eq 'ext_12345' - summary: Filter by external ID - responses: - '200': - description: Retrieved entities - content: - application/json: - schema: - title: Collection of schools - type: object - properties: - value: - type: array - items: - $ref: '#/components/schemas/educationSchool' - example: - - id: 0e19303e-8144-44bd-86d8-35cc8ac9c255 - displayName: Gymnasium Musterstadt - schoolNumber: '0123' - terminationDate: 2023-06-30T00:00:00Z - - id: 43b879c4-14c6-4e0a-9b3f-b1b33c5a4bd4 - displayName: Realschule Musterstadt - schoolNumber: '1234' - default: - $ref: '#/components/responses/error' - post: - tags: - - educationSchool - summary: Add new school - security: - - bearerAuth: [ ] - operationId: CreateSchool - requestBody: - description: New school - content: - application/json: - schema: - $ref: '#/components/schemas/educationSchool' - examples: - school: - $ref: '#/components/examples/educationSchool_post' - required: true - responses: - '201': - description: Created entity - content: - application/json: - schema: - $ref: '#/components/schemas/educationSchool' - examples: - school: - $ref: '#/components/examples/educationSchool' - default: - $ref: '#/components/responses/error' - '/v1.0/education/schools/{school-id}': - get: - tags: - - educationSchool - summary: Get the properties of a specific school - security: - - bearerAuth: [ ] - operationId: GetSchool - parameters: - - name: school-id - in: path - description: 'key: id or schoolNumber of school' - required: true - schema: - type: string - example: 43b879c4-14c6-4e0a-9b3f-b1b33c5a4bd4 - responses: - '200': - description: Retrieved entity - content: - application/json: - schema: - $ref: '#/components/schemas/educationSchool' - examples: - school: - $ref: '#/components/examples/educationSchool' - default: - $ref: '#/components/responses/error' - patch: - tags: - - educationSchool - summary: Update properties of a school - security: - - bearerAuth: [ ] - operationId: UpdateSchool - parameters: - - name: school-id - in: path - description: 'key: id or schoolNumber of school' - required: true - schema: - type: string - example: 43b879c4-14c6-4e0a-9b3f-b1b33c5a4bd4 - requestBody: - description: New property values - content: - application/json: - schema: - $ref: '#/components/schemas/educationSchool' - examples: - schoolNumber: - $ref: '#/components/examples/educationSchool_patchNumber' - schoolTerminationDate: - $ref: '#/components/examples/educationSchool_patchTerminationDate' - deleteSchoolTerminationDate: - $ref: '#/components/examples/educationSchool_deleteTerminationDate' - required: true - responses: - '200': - description: Success - content: - application/json: - schema: - $ref: '#/components/schemas/educationSchool' - examples: - school: - $ref: '#/components/examples/educationSchool' - default: - $ref: '#/components/responses/error' - delete: - tags: - - educationSchool - summary: Delete school - description: Deletes a school. A school can only be delete if it has the terminationDate property set. And if that termination Date is in the past. - security: - - bearerAuth: [ ] - operationId: DeleteSchool - parameters: - - name: school-id - in: path - description: 'key: id or schoolNumber of school' - required: true - schema: - type: string - example: 43b879c4-14c6-4e0a-9b3f-b1b33c5a4bd4 - responses: - '204': - description: Success - default: - $ref: '#/components/responses/error' - '/v1.0/education/schools/{school-id}/users': - get: - tags: - - educationSchool - summary: Get the educationUser resources associated with an educationSchool - security: - - bearerAuth: [ ] - operationId: ListSchoolUsers - parameters: - - name: school-id - in: path - description: 'key: id or schoolNumber of school' - required: true - schema: - type: string - example: 43b879c4-14c6-4e0a-9b3f-b1b33c5a4bd4 - responses: - '200': - description: Retrieved educationUser - content: - application/json: - schema: - title: Collection of educationUser - type: object - properties: - value: - type: array - items: - $ref: '#/components/schemas/educationUser' - example: - - id: 90eedea1-dea1-90ee-a1de-ee90a1deee90 - onPremisesSAMAccountName: max.mustermann - displayName: Max Mustermann - surname: Mustermann - givenName: Max - mail: max.musterman@example.org - primaryRole: student - identities: - - issuer: idp.school.com - issuerAssignedId: max.mustermann - default: - $ref: '#/components/responses/error' - '/v1.0/education/schools/{school-id}/users/$ref': - post: - tags: - - educationSchool - summary: Assign a user to a school - security: - - bearerAuth: [ ] - operationId: AddUserToSchool - parameters: - - name: school-id - in: path - description: 'key: id or schoolNumber of school' - required: true - schema: - type: string - example: 43b879c4-14c6-4e0a-9b3f-b1b33c5a4bd4 - requestBody: - description: educationUser to be added as member - content: - application/json: - schema: - title: EducationUser Reference - type: object - properties: - '@odata.id': - type: string - example: "https:///graph/v1.0/education/users/90eedea1-dea1-90ee-a1de-ee90a1deee90" - required: true - responses: - '204': - description: Success - default: - $ref: '#/components/responses/error' - '/v1.0/education/schools/{school-id}/users/{user-id}/$ref': - delete: - tags: - - educationSchool - summary: Unassign user from a school - security: - - bearerAuth: [ ] - operationId: DeleteUserFromSchool - parameters: - - name: school-id - in: path - description: 'key: id or schoolNumber of school' - required: true - schema: - type: string - example: 43b879c4-14c6-4e0a-9b3f-b1b33c5a4bd4 - - name: user-id - in: path - description: 'key: id or username of the user to unassign from school' - required: true - schema: - type: string - example: 90eedea1-dea1-90ee-a1de-ee90a1deee90 - responses: - '204': - description: Success - default: - $ref: '#/components/responses/error' - '/v1.0/education/schools/{school-id}/classes': - get: - tags: - - educationSchool - summary: Get the educationClass resources owned by an educationSchool - security: - - bearerAuth: [ ] - operationId: ListSchoolClasses - parameters: - - name: school-id - in: path - description: 'key: id or schoolNumber of school' - required: true - schema: - type: string - example: 43b879c4-14c6-4e0a-9b3f-b1b33c5a4bd4 - responses: - '200': - description: Retrieved classes - content: - application/json: - schema: - title: Collection of educationClass - type: object - properties: - value: - type: array - items: - $ref: '#/components/schemas/educationClass' - example: - - id: 86948e45-96a6-43df-b83d-46e92afd30de - displayName: Mathematik - classification: course - externalId: ac528aea-978e-415c-a733-c4ba235d3388 - - id: 9097631c-24b6-4621-9635-36195a9c8d79 - displayName: 10b - classification: class - externalId: 0123_10b - default: - $ref: '#/components/responses/error' - '/v1.0/education/schools/{school-id}/classes/$ref': - post: - tags: - - educationSchool - summary: Assign a class to a school - security: - - bearerAuth: [ ] - operationId: AddClassToSchool - parameters: - - name: school-id - in: path - description: 'key: id or schoolNumber of school' - required: true - schema: - type: string - example: 43b879c4-14c6-4e0a-9b3f-b1b33c5a4bd4 - requestBody: - description: educationClass to be added as member - content: - application/json: - schema: - title: Class Reference - type: object - properties: - '@odata.id': - type: string - example: "https:///graph/v1.0/education/classes/7e84a069-f374-479b-817d-71590117d443" - required: true - responses: - '204': - description: Success - default: - $ref: '#/components/responses/error' - '/v1.0/education/schools/{school-id}/classes/{class-id}/$ref': - delete: - tags: - - educationSchool - summary: Unassign class from a school - security: - - bearerAuth: [ ] - operationId: DeleteClassFromSchool - parameters: - - name: school-id - in: path - description: 'key: id or schoolNumber of school' - required: true - schema: - type: string - example: 43b879c4-14c6-4e0a-9b3f-b1b33c5a4bd4 - - name: class-id - in: path - description: 'key: id or externalId of the class to unassign from school' - required: true - schema: - type: string - example: 7e84a069-f374-479b-817d-71590117d443 - responses: - '204': - description: Success - default: - $ref: '#/components/responses/error' - /v1.0/education/classes: - get: - tags: - - educationClass - summary: list education classes - operationId: ListClasses - security: - - bearerAuth: [ ] - responses: - '200': - description: Retrieved entities - content: - application/json: - schema: - title: Collection of class - type: object - properties: - value: - type: array - items: - $ref: '#/components/schemas/educationClass' - example: - - id: 86948e45-96a6-43df-b83d-46e92afd30de - displayName: Mathematik - classification: course - externalId: ac528aea-978e-415c-a733-c4ba235d3388 - - id: 9097631c-24b6-4621-9635-36195a9c8d79 - displayName: 10b - classification: class - externalId: 0123_10b - default: - $ref: '#/components/responses/error' - post: - tags: - - educationClass - summary: Add new education class - security: - - bearerAuth: [ ] - operationId: CreateClass - requestBody: - description: New entity - content: - application/json: - schema: - $ref: '#/components/schemas/educationClass' - examples: - class: - $ref: '#/components/examples/educationClass_post' - required: true - responses: - '201': - description: Created entity - content: - application/json: - schema: - $ref: '#/components/schemas/educationClass' - examples: - class: - $ref: '#/components/examples/educationClass' - default: - $ref: '#/components/responses/error' - '/v1.0/education/classes/{class-id}': - get: - tags: - - educationClass - summary: Get class by key - security: - - bearerAuth: [ ] - operationId: GetClass - parameters: - - name: class-id - in: path - description: 'key: id or externalId of class' - required: true - schema: - type: string - example: 86948e45-96a6-43df-b83d-46e92afd30de - responses: - '200': - description: Retrieved entity - content: - application/json: - schema: - $ref: '#/components/schemas/educationClass' - examples: - class: - $ref: '#/components/examples/educationClass' - default: - $ref: '#/components/responses/error' - patch: - tags: - - educationClass - summary: Update properties of a education class - security: - - bearerAuth: [ ] - operationId: UpdateClass - parameters: - - name: class-id - in: path - description: 'key: id or externalId of class' - required: true - schema: - type: string - example: 86948e45-96a6-43df-b83d-46e92afd30de - requestBody: - description: New property values - content: - application/json: - schema: - $ref: '#/components/schemas/educationClass' - examples: - update name: - value: - displayName: Musik - add multiple members: - value: - '@members@odata.bind': - - "https://localhost:9200/graph/v1.0/education/users/4c510ada-c86b-4815-8820-42cdf82c3d51" - - "https://localhost:9200/graph/v1.0/education/users/c54b0588-7157-4521-bb52-c1c8ca84ea71" - required: true - responses: - '200': - description: New property values - content: - application/json: - schema: - $ref: '#/components/schemas/educationClass' - example: - id: 86948e45-96a6-43df-b83d-46e92afd30de - displayName: Musik - classification: course - externalId: cb766da3-bab1-478a-9615-f99d4c59c09a - '204': - description: Success - default: - $ref: '#/components/responses/error' - delete: - tags: - - educationClass - summary: Delete education class - security: - - bearerAuth: [ ] - operationId: DeleteClass - parameters: - - name: class-id - in: path - description: 'key: id or externalId of class' - required: true - schema: - type: string - example: 86948e45-96a6-43df-b83d-46e92afd30de - responses: - '204': - description: Success - default: - $ref: '#/components/responses/error' - '/v1.0/education/classes/{class-id}/members': - get: - tags: - - educationClass - summary: Get the educationClass resources owned by an educationSchool - security: - - bearerAuth: [ ] - operationId: ListClassMembers - parameters: - - name: class-id - in: path - description: 'key: id or externalId of class' - required: true - schema: - type: string - example: 86948e45-96a6-43df-b83d-46e92afd30de - responses: - '200': - description: Retrieved class members - content: - application/json: - schema: - title: Collection of educationUser - type: object - properties: - value: - type: array - items: - $ref: '#/components/schemas/educationUser' - example: - - id: 90eedea1-dea1-90ee-a1de-ee90a1deee90 - onPremisesSAMAccountName: max.mustermann - displayName: Max Mustermann - surname: Mustermann - givenName: Max - mail: max.musterman@example.org - primaryRole: student - identities: - - issuer: idp.school.com - issuerAssignedId: max.mustermann - default: - $ref: '#/components/responses/error' - '/v1.0/education/classes/{class-id}/members/$ref': - post: - tags: - - educationClass - summary: Assign a user to a class - security: - - bearerAuth: [ ] - operationId: AddUserToClass - parameters: - - name: class-id - in: path - description: 'key: id or externalId of class' - required: true - schema: - type: string - example: 86948e45-96a6-43df-b83d-46e92afd30de - requestBody: - description: educationUser to be added as member - content: - application/json: - schema: - title: Class Member Reference - type: object - properties: - '@odata.id': - type: string - example: "https:///graph/v1.0/education/users/90eedea1-dea1-90ee-a1de-ee90a1deee90" - required: true - responses: - '204': - description: Success - default: - $ref: '#/components/responses/error' - '/v1.0/education/classes/{class-id}/members/{user-id}/$ref': - delete: - tags: - - educationClass - summary: Unassign user from a class - security: - - bearerAuth: [ ] - operationId: DeleteUserFromClass - parameters: - - name: class-id - in: path - description: 'key: id or externalId of class' - required: true - schema: - type: string - - name: user-id - in: path - description: 'key: id or username of the user to unassign from class' - required: true - schema: - type: string - example: 90eedea1-dea1-90ee-a1de-ee90a1deee90 - responses: - '204': - description: Success - default: - $ref: '#/components/responses/error' - '/v1.0/education/classes/{class-id}/teachers': - get: - tags: - - educationClass.teachers - summary: Get the teachers for a class - security: - - bearerAuth: [ ] - operationId: GetTeachers - parameters: - - name: class-id - in: path - description: 'key: id or externalId of class' - required: true - schema: - type: string - example: 86948e45-96a6-43df-b83d-46e92afd30de - responses: - '200': - description: Retrieved class teachers - content: - application/json: - schema: - title: Collection of educationUser - type: object - properties: - value: - type: array - items: - $ref: '#/components/schemas/educationUser' - example: - - id: 90eedea1-dea1-90ee-a1de-ee90a1deee90 - onPremisesSAMAccountName: max.mustermann - displayName: Max Mustermann - surname: Mustermann - givenName: Max - mail: max.musterman@example.org - primaryRole: teacher - identities: - - issuer: idp.school.com - issuerAssignedId: max.mustermann - default: - $ref: '#/components/responses/error' - '/v1.0/education/classes/{class-id}/teachers/$ref': - post: - tags: - - educationClass.teachers - summary: Assign a teacher to a class - security: - - bearerAuth: [ ] - operationId: AddTeacherToClass - parameters: - - name: class-id - in: path - description: 'key: id or externalId of class' - required: true - schema: - type: string - example: 86948e45-96a6-43df-b83d-46e92afd30de - requestBody: - description: educationUser to be added as teacher - content: - application/json: - schema: - title: Class Teacher Reference - type: object - properties: - '@odata.id': - type: string - example: "https:///graph/v1.0/education/users/90eedea1-dea1-90ee-a1de-ee90a1deee90" - required: true - responses: - '204': - description: Success - default: - $ref: '#/components/responses/error' - '/v1.0/education/classes/{class-id}/teachers/{user-id}/$ref': - delete: - tags: - - educationClass.teachers - summary: Unassign user as teacher of a class - security: - - bearerAuth: [ ] - operationId: DeleteTeacherFromClass - parameters: - - name: class-id - in: path - description: 'key: id or externalId of class' - required: true - schema: - type: string - - name: user-id - in: path - description: 'key: id or username of the user to unassign as teacher' - required: true - schema: - type: string - example: 90eedea1-dea1-90ee-a1de-ee90a1deee90 - responses: - '204': - description: Success - default: - $ref: '#/components/responses/error' - '/v1.0/extensions/org.libregraph/tags': - get: - tags: - - tags - summary: Get all known tags - operationId: GetTags - responses: - '200': - description: Retrieved tags - content: - application/json: - schema: - title: Collection of tags - type: object - properties: - value: - type: array - items: - type: string - default: - $ref: '#/components/responses/error' - put: - tags: - - tags - summary: Assign tags to a resource - operationId: AssignTags - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/tagAssignment' - examples: - assign: - $ref: '#/components/examples/tags_put' - responses: - '200': - description: No content - default: - $ref: '#/components/responses/error' - delete: - tags: - - tags - summary: Unassign tags from a resource - operationId: UnassignTags - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/tagUnassignment' - examples: - unassign: - $ref: '#/components/examples/tags_delete' - responses: - '200': - description: No content - default: - $ref: '#/components/responses/error' - /v1.0/applications: - get: - tags: - - applications - summary: Get all applications - operationId: ListApplications - responses: - '200': - description: Retrieved entities - content: - application/json: - schema: - title: Collection of applications - type: object - properties: - value: - type: array - items: - $ref: '#/components/schemas/application' - default: - $ref: '#/components/responses/error' - /v1.0/applications/{application-id}: - get: - tags: - - applications - summary: Get application by id - operationId: GetApplication - parameters: - - name: application-id - in: path - description: 'key: id of application' - required: true - schema: - type: string - responses: - '200': - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/application' - default: - $ref: '#/components/responses/error' - /v1beta1/roleManagement/permissions/roleDefinitions: - get: - tags: - - roleManagement - summary: List roleDefinitions - operationId: ListPermissionRoleDefinitions - description: | - Get a list of `unifiedRoleDefinition` objects for the permissions provider. This list determines the roles that can be selected when creating sharing invites. - responses: - '200': - description: A list of permission roles than can be used when sharing with users or groups. - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/unifiedRoleDefinition' - example: - - id: b1e2218d-eef8-4d4c-b82d-0f1a1b48f3b5 - description: "Allows reading the shared file or folder" - displayName: Viewer - rolePermissions: - - allowedResourceActions: - - libre.graph/driveItem/basic/read - - libre.graph/driveItem/permissions/read - - libre.graph/driveItem/upload/create - condition: "exists @Resource.File" - '@libre.graph.weight': 1 - - id: fb6c3e19-e378-47e5-b277-9732f9de6e21 - description: "Allows reading and writing the shared file or folder" - displayName: Editor - rolePermissions: - - allowedResourceActions: - - libre.graph/driveItem/standard/allTasks - condition: "exists @Resource.File" - '@libre.graph.weight': 2 - - id: 312c0871-5ef7-4b3a-85b6-0e4074c64049 - description: "Allows managing a space" - displayName: Manager - rolePermissions: - - allowedResourceActions: - - libre.graph/drive/standard/allTasks - condition: "exists @Resource.File" - '@libre.graph.weight': 3 - - id: 4916f47e-66d5-49bb-9ac9-748ad00334b - description: "Allows creating new files" - displayName: File Drop - rolePermissions: - - allowedResourceActions: - - libre.graph/driveItem/upload/create - condition: "exists @Resource.File" - '@libre.graph.weight': 4 - default: - $ref: '#/components/responses/error' - /v1beta1/roleManagement/permissions/roleDefinitions/{role-id}: - get: - tags: - - roleManagement - summary: Get unifiedRoleDefinition - operationId: GetPermissionRoleDefinition - description: | - Read the properties and relationships of a `unifiedRoleDefinition` object. - parameters: - - name: role-id - in: path - description: 'key: id of roleDefinition' - required: true - schema: - type: string - x-ms-docs-key-type: roleDefinition - responses: - '200': - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/unifiedRoleDefinition' - examples: - Viewer: - value: - id: b1e2218d-eef8-4d4c-b82d-0f1a1b48f3b5 - description: "Allows reading the shared file or folder" - displayName: Viewer - rolePermissions: - - allowedResourceActions: - - libre.graph/driveItem/basic/read - condition: "exists @Resource.File" - '@libre.graph.weight': 1 - Editor: - value: - id: fb6c3e19-e378-47e5-b277-9732f9de6e21 - description: "Allows reading and writing the shared file or folder" - displayName: Editor - rolePermissions: - - allowedResourceActions: - - libre.graph/driveItem/standard/allTasks - condition: "exists @Resource.File" - '@libre.graph.weight': 2 - Manager: - value: - id: 312c0871-5ef7-4b3a-85b6-0e4074c64049 - description: "Allows managing a space" - displayName: Manager - rolePermissions: - - allowedResourceActions: - - libre.graph/drive/standard/allTasks - condition: "exists @Resource.File" - '@libre.graph.weight': 3 - File Drop: - value: - id: 4916f47e-66d5-49bb-9ac9-748ad00334b - description: "Allows creating new files" - displayName: File Drop - rolePermissions: - - allowedResourceActions: - - libre.graph/driveItem/standard/create - condition: "exists @Resource.File" - '@libre.graph.weight': 4 - default: - $ref: '#/components/responses/error' -components: - schemas: - tagAssignment: - type: object - required: - - resourceId - - tags - properties: - resourceId: - type: string - tags: - type: array - items: - type: string - tagUnassignment: - type: object - required: - - resourceId - - tags - properties: - resourceId: - type: string - tags: - type: array - items: - type: string - educationSchool: - description: Represents a school - type: object - properties: - id: - type: string - description: The unique identifier for an entity. Read-only. - readOnly: true - displayName: - type: string - description: The organization name - schoolNumber: - type: string - description: School number - externalId: - type: string - description: External identifier of the school - terminationDate: - type: string - format: date-time - nullable: true - description: Date and time at which the service for this organization is scheduled to be terminated - educationClass: - description: And extension of group representing a class or course - type: object - required: - - displayName - - classification - properties: - # entity - id: - type: string - description: Read-only. - readOnly: true - # group - description: - type: string - description: 'An optional description for the group. Returned by default.' - displayName: - type: string - description: 'The display name for the group. This property is required when a group is created and cannot be cleared during updates. Returned by default. Supports $search and $orderBy.' - members: - type: array - items: - $ref: '#/components/schemas/user' - description: 'Users and groups that are members of this group. HTTP Methods: GET (supported for all groups), Nullable. Supports $expand.' - 'members@odata.bind': - type: array - items: - type: string - uniqueItems: true - minItems: 1 - maxItems: 20 - description: A list of member references to the members to be added. Up to 20 members can be added with a single request - classification: - type: string - enum: [ class, course ] - description: 'Classification of the group, i.e. "class" or "course"' - externalId: - type: string - description: An external unique ID for the class - educationUser: - description: An extension of user with education-specific attributes - type: object - properties: - # entity - id: - type: string - description: Read-only. - readOnly: true - # user - accountEnabled: - type: boolean - description: 'Set to "true" when the account is enabled.' - displayName: - type: string - description: 'The name displayed in the address book for the user. This value is usually the combination of the user''s first name, middle initial, and last name. This property is required when a user is created and it cannot be cleared during updates. Returned by default. Supports $orderby.' - drives: - type: array - items: - $ref: '#/components/schemas/drive' - description: A collection of drives available for this user. Read-only. - maxItems: 100 - readOnly: true - drive: - $ref: '#/components/schemas/drive' - description: The personal drive of this user. Read-only. - readOnly: true - externalId: - type: string - description: An external unique ID for the user. Use it to associate a user in another system, such as a student or employee ID number. - identities: - description: Identities associated with this account. - type: array - items: - $ref: '#/components/schemas/objectIdentity' - mail: - type: string - description: 'The SMTP address for the user, for example, ''jeff@contoso.opencloud.com''. Returned by default.' - memberOf: - type: array - items: - $ref: '#/components/schemas/group' - description: 'Groups that this user is a member of. HTTP Methods: GET (supported for all groups). Read-only. Nullable. Supports $expand.' - onPremisesSamAccountName: - type: string - description: 'Contains the on-premises SAM account name synchronized from the on-premises directory. Read-only.' - passwordProfile: - $ref: '#/components/schemas/passwordProfile' - surname: - type: string - description: The user's surname (family name or last name). Returned by default. - givenName: - type: string - description: The user's givenName. Returned by default. - primaryRole: - type: string - description: 'The user`s default role. Such as "student" or "teacher"' - userType: - type: string - description: 'The user`s type. This can be either "Member" for regular user, "Guest" for guest users or "Federated" for users imported from a federated instance.' - drive: - description: The drive represents a space on the storage. - required: - - name - allOf: - - $ref: '#/components/schemas/driveUpdate' - driveUpdate: - description: The drive represents an update to a space on the storage. - type: object - properties: - # entity - id: - type: string - description: The unique identifier for this drive. - readOnly: true - # base item - createdBy: - $ref: '#/components/schemas/identitySet' - description: 'Identity of the user, device, or application which created the item. Read-only.' - readOnly: true - createdDateTime: - pattern: '^[0-9]{4,}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]([.][0-9]{1,12})?([Zz]|[+-][0-9][0-9]:[0-9][0-9])$' - type: string - description: Date and time of item creation. Read-only. - format: date-time - readOnly: true - description: - type: string - description: Provides a user-visible description of the item. Optional. - eTag: - type: string - description: ETag for the item. Read-only. - readOnly: true - lastModifiedBy: - $ref: '#/components/schemas/identitySet' - description: 'Identity of the user, device, and application which last modified the item. Read-only.' - readOnly: true - lastModifiedDateTime: - pattern: '^[0-9]{4,}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]([.][0-9]{1,12})?([Zz]|[+-][0-9][0-9]:[0-9][0-9])$' - type: string - description: Date and time the item was last modified. Read-only. - format: date-time - readOnly: true - name: - type: string - description: The name of the item. Read-write. - parentReference: - $ref: '#/components/schemas/itemReference' - description: 'Parent information, if the item has a parent. Read-write.' - webUrl: - type: string - description: URL that displays the resource in the browser. Read-only. - readOnly: true - # drive - driveType: - type: string - description: Describes the type of drive represented by this resource. Values are "personal" for users home spaces, "project", "virtual" or "share". Read-only. - readOnly: true - driveAlias: - type: string - description: "The drive alias can be used in clients to make the urls user friendly. Example: 'personal/einstein'. This will be used to resolve to the correct driveID." - owner: - $ref: '#/components/schemas/identitySet' - quota: - $ref: '#/components/schemas/quota' - items: - type: array - items: - $ref: '#/components/schemas/driveItem' - description: All items contained in the drive. Read-only. Nullable. - readOnly: true - root: - $ref: '#/components/schemas/driveItem' - description: Drive item describing the drive's root. Read-only. - special: - type: array - items: - $ref: '#/components/schemas/driveItem' - description: A collection of special drive resources. - '@libre.graph.hasTrashedItems': - type: boolean - description: Indicates whether the drive has items in the trash. Read-only. - readOnly: true - driveRecipient: - type: object - description: | - Represents a person, group, or other recipient to share a drive item with using the invite action. - - When using invite to add permissions, the `driveRecipient` object would specify the `email`, `alias`, - or `objectId` of the recipient. Only one of these values is required; multiple values are not accepted. - properties: - #alias: - # type: string - # description: The alias of the domain object, for cases where an email address is unavailable (e.g. groups). - #email: - # type: string - # description: The email address for the recipient, if the recipient has an associated email address. - objectId: - type: string - description: The unique identifier for the recipient in the directory. - "@libre.graph.recipient.type": - type: string - description: When the recipient is referenced by objectId this annotation is used to differentiate `user` and `group` recipients. - default: user - identitySet: - type: object - description: Optional. User account. - properties: - application: - $ref: '#/components/schemas/identity' - description: Optional. The application associated with this action. - device: - $ref: '#/components/schemas/identity' - description: Optional. The device associated with this action. - user: - $ref: '#/components/schemas/identity' - description: Optional. The user associated with this action. - # this is not part of the ms graph api. there, only the sharepointIdentitySet used in grantedToV2 has a group - group: - $ref: '#/components/schemas/identity' - description: Optional. The group associated with this action. - sharePointIdentitySet: - type: object - description: This resource is used to represent a set of identities associated with various events for an item, such as created by or last modified by. - properties: - user: - $ref: '#/components/schemas/identity' - description: Optional. The user associated with this action. - group: - $ref: '#/components/schemas/identity' - description: Optional. The group associated with this action. - - # unused - #application: - # $ref: '#/components/schemas/identity' - # description: Optional. The application associated with this action. - - # unused - #device: - # $ref: '#/components/schemas/identity' - # description: Optional. The device associated with this action. - # other properties omitted for now - identity: - type: object - required: - - displayName - properties: - displayName: - type: string - description: 'The identity''s display name. Note that this may not always be available or up to date. For example, if a user changes their display name, the API may show the new value in a future response, but the items associated with the user won''t show up as having changed when using delta.' - id: - type: string - description: Unique identifier for the identity. - '@libre.graph.userType': - type: string - description: 'The type of the identity. This can be either "Member" for regular user, "Guest" for guest users or "Federated" for users imported from a federated instance. Can be used by clients to indicate the type of user. For more details, clients should look up and cache the user at the /users endpoint.' - invitation: - type: object - description: | - Represents an invitation to a drive item. - properties: - invitedUserDisplayName: - type: string - description: The display name of the user being invited. - invitedUserEmailAddress: - type: string - description: The email address of the user being invited. Required. - invitedUserMessageInfo: - $ref: '#/components/schemas/invitedUserMessageInfo' - description: Additional information about the invitation message. - sendInvitationMessage: - type: boolean - description: Indicates whether an invitation message should be sent to the user. - inviteRedirectUrl: - type: string - description: The URL to which the user is redirected after accepting the invitation. Required. - inviteRedeemUrl: - type: string - description: The URL that the user can use to redeem the invitation. Read-only. - readOnly: true - status: - type: string - description: The status of the invitation. Read-only. - readOnly: true - invitedUser: - $ref: '#/components/schemas/user' - description: The user object of the invited user. Read-only. - readOnly: true - invitedUserType: - type: string - description: The type of user being invited. - invitedUserMessageInfo: - type: object - description: Additional information about the invitation message. - properties: - ccRecipients: - type: array - items: - $ref: '#/components/schemas/recipient' - description: Additional recipients who will receive a copy of the invitation message. - customizedMessageBody: - type: string - description: The customized message body that will be included in the invitation message. - messageLanguage: - type: string - description: The language of the invitation message. - recipient: - type: object - description: Represents a recipient of an invitation. - properties: - emailAddress: - $ref: '#/components/schemas/emailAddress' - description: The email address of the recipient. - emailAddress: - type: object - description: Represents an email address. - properties: - address: - type: string - description: The email address. - name: - type: string - description: The name associated with the email address. - - objectIdentity: - description: Represents an identity used to sign in to a user account - type: object - properties: - issuer: - description: domain of the Provider issuing the identity - type: string - issuerAssignedId: - description: The unique id assigned by the issuer to the account - type: string - passwordProfile: - description: Password Profile associated with a user - type: object - properties: - forceChangePasswordNextSignIn: - type: boolean - description: If true the user is required to change their password upon the next login - default: false - password: - type: string - writeOnly: true - description: The user's password - sharingLink: - description: | - The `SharingLink` resource groups link-related data items into a single structure. - - If a `permission` resource has a non-null `sharingLink` facet, the permission represents a sharing link (as opposed to permissions granted to a person or group). - type: object - properties: - type: - $ref: '#/components/schemas/sharingLinkType' - preventsDownload: - type: boolean - description: If `true` then the user can only use this link to view the item on the web, and cannot use it to download the contents of the item. - readOnly: true - webUrl: - type: string - description: A URL that opens the item in the browser on the website. - readOnly: true - '@libre.graph.displayName': - type: string - description: Provides a user-visible display name of the link. Optional. Libregraph only. - '@libre.graph.quickLink': - type: boolean - description: The quicklink property can be assigned to only one link per resource. A quicklink can be used in the clients to provide a one-click copy to clipboard action. Optional. Libregraph only. - # unused - #webHtml: - # type: string - # description: For `embed` links, this property contains the HTML code for an `