diff --git a/README.md b/README.md index e030c33..26f4bcd 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,15 @@ Node.js TypeScript sluzba pro komunikaci s Google API za AppFactory reverse prox - `POST /google/oauth/service-account-token` - `POST /google/oauth/revoke` - `GET /google/oauth/tokeninfo` +- `GET|POST|PATCH|DELETE /google/calendar/...` +- `GET|POST|PUT /google/sheets/...` +- `GET|POST|PATCH|DELETE /google/drive/...` +- `GET|POST /google/gmail/...` +- `GET|POST /google/docs/...` +- `GET|POST /google/slides/...` +- `GET|POST /google/forms/...` +- `GET|POST|PATCH|DELETE /google/people/...` +- `GET|POST|PATCH|DELETE /google/tasks/...` - `POST /google/request` ## Konfigurace @@ -50,3 +59,46 @@ Priklad: "accessTokenEnv": "GOOGLE_ACCESS_TOKEN" } ``` + +## Konkretni Google API wrappery + +Sluzba ma konkretni endpointy pro bezne Google produkty, aby klient nemusel skladat cilove Google URL sam: + +- Calendar: kalendare, eventy, presun eventu +- Sheets: spreadsheet metadata, batch update, cteni hodnot, update hodnot, append, batch clear +- Drive: soubory, export, opravneni +- Gmail: profil, zpravy, odeslani zpravy, labels +- Docs: vytvoreni dokumentu, nacteni dokumentu, batch update +- Slides: vytvoreni prezentace, nacteni prezentace, batch update +- Forms: vytvoreni formulare, nacteni formulare, batch update, responses +- People: connections, kontakt, vytvoreni/uprava/smazani kontaktu +- Tasks: task lists, tasky, uprava/smazani tasku + +Autorizace je stejna jako u obecne proxy: `Authorization: Bearer `, pripadne `accessToken`, `accessTokenEnv`, `apiKey` nebo `apiKeyEnv` v JSON body. + +Priklad vytvoreni udalosti: + +```json +POST /google/calendar/calendars/primary/events +{ + "summary": "Schuzka", + "start": { + "dateTime": "2026-06-15T10:00:00+02:00" + }, + "end": { + "dateTime": "2026-06-15T11:00:00+02:00" + } +} +``` + +Priklad append do Google Sheets: + +```json +POST /google/sheets/spreadsheets/{spreadsheetId}/values/append?range=Sheet1!A1 +{ + "majorDimension": "ROWS", + "values": [ + ["A", "B", "C"] + ] +} +``` diff --git a/documentation/google-api-communication.md b/documentation/google-api-communication.md index 3f20130..941acef 100644 --- a/documentation/google-api-communication.md +++ b/documentation/google-api-communication.md @@ -44,6 +44,83 @@ Priklad volani Google Drive: Z bezpecnostnich duvodu jsou povolene jen HTTPS URL na Google domenach. Requesty na jine hosty sluzba odmita. +## Konkretni produktove endpointy + +Krome obecne proxy jsou dostupne konkretni wrappery nad nejbeznejsimi Google API: + +### Calendar + +- `GET /google/calendar/calendars` +- `POST /google/calendar/calendars` +- `GET /google/calendar/calendars/{calendarId}` +- `PATCH /google/calendar/calendars/{calendarId}` +- `DELETE /google/calendar/calendars/{calendarId}` +- `GET /google/calendar/calendars/{calendarId}/events` +- `POST /google/calendar/calendars/{calendarId}/events` +- `GET /google/calendar/calendars/{calendarId}/events/{eventId}` +- `PATCH /google/calendar/calendars/{calendarId}/events/{eventId}` +- `DELETE /google/calendar/calendars/{calendarId}/events/{eventId}` +- `POST /google/calendar/calendars/{calendarId}/events/{eventId}/move` + +### Sheets + +- `POST /google/sheets/spreadsheets` +- `GET /google/sheets/spreadsheets/{spreadsheetId}` +- `POST /google/sheets/spreadsheets/{spreadsheetId}/batch-update` +- `GET /google/sheets/spreadsheets/{spreadsheetId}/values?range=Sheet1!A1:B10` +- `PUT /google/sheets/spreadsheets/{spreadsheetId}/values?range=Sheet1!A1` +- `POST /google/sheets/spreadsheets/{spreadsheetId}/values/append?range=Sheet1!A1` +- `POST /google/sheets/spreadsheets/{spreadsheetId}/values/batch-update` +- `POST /google/sheets/spreadsheets/{spreadsheetId}/values/batch-clear` + +### Drive + +- `GET /google/drive/files` +- `POST /google/drive/files` +- `GET /google/drive/files/{fileId}` +- `PATCH /google/drive/files/{fileId}` +- `DELETE /google/drive/files/{fileId}` +- `GET /google/drive/files/{fileId}/export?mimeType=application/pdf` +- `GET /google/drive/files/{fileId}/permissions` +- `POST /google/drive/files/{fileId}/permissions` +- `DELETE /google/drive/files/{fileId}/permissions/{permissionId}` + +### Gmail + +- `GET /google/gmail/profile` +- `GET /google/gmail/messages` +- `GET /google/gmail/messages/{messageId}` +- `POST /google/gmail/messages/send` +- `GET /google/gmail/labels` +- `POST /google/gmail/labels` + +### Docs, Slides a Forms + +- `POST /google/docs/documents` +- `GET /google/docs/documents/{documentId}` +- `POST /google/docs/documents/{documentId}/batch-update` +- `POST /google/slides/presentations` +- `GET /google/slides/presentations/{presentationId}` +- `POST /google/slides/presentations/{presentationId}/batch-update` +- `POST /google/forms/forms` +- `GET /google/forms/forms/{formId}` +- `POST /google/forms/forms/{formId}/batch-update` +- `GET /google/forms/forms/{formId}/responses` + +### People a Tasks + +- `GET /google/people/connections` +- `GET /google/people/{resourceName}` +- `POST /google/people/contacts` +- `PATCH /google/people/{resourceName}` +- `DELETE /google/people/{resourceName}` +- `GET /google/tasks/lists` +- `POST /google/tasks/lists` +- `GET /google/tasks/lists/{tasklistId}/tasks` +- `POST /google/tasks/lists/{tasklistId}/tasks` +- `PATCH /google/tasks/lists/{tasklistId}/tasks/{taskId}` +- `DELETE /google/tasks/lists/{tasklistId}/tasks/{taskId}` + ## AppFactory overeni Po deploy over: diff --git a/src/index.ts b/src/index.ts index ea07aed..c207fc1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -35,6 +35,14 @@ type ServiceAccountTokenBody = { privateKey?: string; }; +type GoogleRouteOptions = { + baseUrl: string; + path: string; + method?: string; + query?: Record; + body?: JsonValue; +}; + const app = express(); const port = Number(process.env.PORT || 3000); const rootPath = normalizeRootPath(process.env.ROOT_PATH || ""); @@ -261,6 +269,435 @@ app.get("/google/oauth/tokeninfo", async (req, res) => { } }); +app.get("/google/calendar/calendars", async (req, res) => { + await callGoogle(req, res, { baseUrl: "https://www.googleapis.com", path: "/calendar/v3/users/me/calendarList" }); +}); + +app.post("/google/calendar/calendars", async (req, res) => { + await callGoogle(req, res, { + baseUrl: "https://www.googleapis.com", + path: "/calendar/v3/calendars", + method: "POST", + body: req.body as JsonValue + }); +}); + +app.get("/google/calendar/calendars/:calendarId", async (req, res) => { + await callGoogle(req, res, { + baseUrl: "https://www.googleapis.com", + path: `/calendar/v3/calendars/${encodePathParam(req.params.calendarId)}` + }); +}); + +app.patch("/google/calendar/calendars/:calendarId", async (req, res) => { + await callGoogle(req, res, { + baseUrl: "https://www.googleapis.com", + path: `/calendar/v3/calendars/${encodePathParam(req.params.calendarId)}`, + method: "PATCH", + body: req.body as JsonValue + }); +}); + +app.delete("/google/calendar/calendars/:calendarId", async (req, res) => { + await callGoogle(req, res, { + baseUrl: "https://www.googleapis.com", + path: `/calendar/v3/calendars/${encodePathParam(req.params.calendarId)}`, + method: "DELETE" + }); +}); + +app.get("/google/calendar/calendars/:calendarId/events", async (req, res) => { + await callGoogle(req, res, { + baseUrl: "https://www.googleapis.com", + path: `/calendar/v3/calendars/${encodePathParam(req.params.calendarId)}/events` + }); +}); + +app.post("/google/calendar/calendars/:calendarId/events", async (req, res) => { + await callGoogle(req, res, { + baseUrl: "https://www.googleapis.com", + path: `/calendar/v3/calendars/${encodePathParam(req.params.calendarId)}/events`, + method: "POST", + body: req.body as JsonValue + }); +}); + +app.get("/google/calendar/calendars/:calendarId/events/:eventId", async (req, res) => { + await callGoogle(req, res, { + baseUrl: "https://www.googleapis.com", + path: `/calendar/v3/calendars/${encodePathParam(req.params.calendarId)}/events/${encodePathParam(req.params.eventId)}` + }); +}); + +app.patch("/google/calendar/calendars/:calendarId/events/:eventId", async (req, res) => { + await callGoogle(req, res, { + baseUrl: "https://www.googleapis.com", + path: `/calendar/v3/calendars/${encodePathParam(req.params.calendarId)}/events/${encodePathParam(req.params.eventId)}`, + method: "PATCH", + body: req.body as JsonValue + }); +}); + +app.delete("/google/calendar/calendars/:calendarId/events/:eventId", async (req, res) => { + await callGoogle(req, res, { + baseUrl: "https://www.googleapis.com", + path: `/calendar/v3/calendars/${encodePathParam(req.params.calendarId)}/events/${encodePathParam(req.params.eventId)}`, + method: "DELETE" + }); +}); + +app.post("/google/calendar/calendars/:calendarId/events/:eventId/move", async (req, res) => { + await callGoogle(req, res, { + baseUrl: "https://www.googleapis.com", + path: `/calendar/v3/calendars/${encodePathParam(req.params.calendarId)}/events/${encodePathParam(req.params.eventId)}/move`, + method: "POST" + }); +}); + +app.post("/google/sheets/spreadsheets", async (req, res) => { + await callGoogle(req, res, { + baseUrl: "https://sheets.googleapis.com", + path: "/v4/spreadsheets", + method: "POST", + body: req.body as JsonValue + }); +}); + +app.get("/google/sheets/spreadsheets/:spreadsheetId", async (req, res) => { + await callGoogle(req, res, { + baseUrl: "https://sheets.googleapis.com", + path: `/v4/spreadsheets/${encodePathParam(req.params.spreadsheetId)}` + }); +}); + +app.post("/google/sheets/spreadsheets/:spreadsheetId/batch-update", async (req, res) => { + await callGoogle(req, res, { + baseUrl: "https://sheets.googleapis.com", + path: `/v4/spreadsheets/${encodePathParam(req.params.spreadsheetId)}:batchUpdate`, + method: "POST", + body: req.body as JsonValue + }); +}); + +app.get("/google/sheets/spreadsheets/:spreadsheetId/values", async (req, res) => { + const range = requireQueryString(req, res, "range"); + if (!range) return; + await callGoogle(req, res, { + baseUrl: "https://sheets.googleapis.com", + path: `/v4/spreadsheets/${encodePathParam(req.params.spreadsheetId)}/values/${encodePathParam(range)}` + }); +}); + +app.put("/google/sheets/spreadsheets/:spreadsheetId/values", async (req, res) => { + const range = requireQueryString(req, res, "range"); + if (!range) return; + await callGoogle(req, res, { + baseUrl: "https://sheets.googleapis.com", + path: `/v4/spreadsheets/${encodePathParam(req.params.spreadsheetId)}/values/${encodePathParam(range)}`, + method: "PUT", + body: req.body as JsonValue + }); +}); + +app.post("/google/sheets/spreadsheets/:spreadsheetId/values/append", async (req, res) => { + const range = requireQueryString(req, res, "range"); + if (!range) return; + await callGoogle(req, res, { + baseUrl: "https://sheets.googleapis.com", + path: `/v4/spreadsheets/${encodePathParam(req.params.spreadsheetId)}/values/${encodePathParam(range)}:append`, + method: "POST", + body: req.body as JsonValue + }); +}); + +app.post("/google/sheets/spreadsheets/:spreadsheetId/values/batch-update", async (req, res) => { + await callGoogle(req, res, { + baseUrl: "https://sheets.googleapis.com", + path: `/v4/spreadsheets/${encodePathParam(req.params.spreadsheetId)}/values:batchUpdate`, + method: "POST", + body: req.body as JsonValue + }); +}); + +app.post("/google/sheets/spreadsheets/:spreadsheetId/values/batch-clear", async (req, res) => { + await callGoogle(req, res, { + baseUrl: "https://sheets.googleapis.com", + path: `/v4/spreadsheets/${encodePathParam(req.params.spreadsheetId)}/values:batchClear`, + method: "POST", + body: req.body as JsonValue + }); +}); + +app.get("/google/drive/files", async (req, res) => { + await callGoogle(req, res, { baseUrl: "https://www.googleapis.com", path: "/drive/v3/files" }); +}); + +app.post("/google/drive/files", async (req, res) => { + await callGoogle(req, res, { + baseUrl: "https://www.googleapis.com", + path: "/drive/v3/files", + method: "POST", + body: req.body as JsonValue + }); +}); + +app.get("/google/drive/files/:fileId", async (req, res) => { + await callGoogle(req, res, { + baseUrl: "https://www.googleapis.com", + path: `/drive/v3/files/${encodePathParam(req.params.fileId)}` + }); +}); + +app.patch("/google/drive/files/:fileId", async (req, res) => { + await callGoogle(req, res, { + baseUrl: "https://www.googleapis.com", + path: `/drive/v3/files/${encodePathParam(req.params.fileId)}`, + method: "PATCH", + body: req.body as JsonValue + }); +}); + +app.delete("/google/drive/files/:fileId", async (req, res) => { + await callGoogle(req, res, { + baseUrl: "https://www.googleapis.com", + path: `/drive/v3/files/${encodePathParam(req.params.fileId)}`, + method: "DELETE" + }); +}); + +app.get("/google/drive/files/:fileId/export", async (req, res) => { + await callGoogle(req, res, { + baseUrl: "https://www.googleapis.com", + path: `/drive/v3/files/${encodePathParam(req.params.fileId)}/export` + }); +}); + +app.get("/google/drive/files/:fileId/permissions", async (req, res) => { + await callGoogle(req, res, { + baseUrl: "https://www.googleapis.com", + path: `/drive/v3/files/${encodePathParam(req.params.fileId)}/permissions` + }); +}); + +app.post("/google/drive/files/:fileId/permissions", async (req, res) => { + await callGoogle(req, res, { + baseUrl: "https://www.googleapis.com", + path: `/drive/v3/files/${encodePathParam(req.params.fileId)}/permissions`, + method: "POST", + body: req.body as JsonValue + }); +}); + +app.delete("/google/drive/files/:fileId/permissions/:permissionId", async (req, res) => { + await callGoogle(req, res, { + baseUrl: "https://www.googleapis.com", + path: `/drive/v3/files/${encodePathParam(req.params.fileId)}/permissions/${encodePathParam(req.params.permissionId)}`, + method: "DELETE" + }); +}); + +app.get("/google/gmail/profile", async (req, res) => { + await callGoogle(req, res, { baseUrl: "https://gmail.googleapis.com", path: "/gmail/v1/users/me/profile" }); +}); + +app.get("/google/gmail/messages", async (req, res) => { + await callGoogle(req, res, { baseUrl: "https://gmail.googleapis.com", path: "/gmail/v1/users/me/messages" }); +}); + +app.get("/google/gmail/messages/:messageId", async (req, res) => { + await callGoogle(req, res, { + baseUrl: "https://gmail.googleapis.com", + path: `/gmail/v1/users/me/messages/${encodePathParam(req.params.messageId)}` + }); +}); + +app.post("/google/gmail/messages/send", async (req, res) => { + await callGoogle(req, res, { + baseUrl: "https://gmail.googleapis.com", + path: "/gmail/v1/users/me/messages/send", + method: "POST", + body: req.body as JsonValue + }); +}); + +app.get("/google/gmail/labels", async (req, res) => { + await callGoogle(req, res, { baseUrl: "https://gmail.googleapis.com", path: "/gmail/v1/users/me/labels" }); +}); + +app.post("/google/gmail/labels", async (req, res) => { + await callGoogle(req, res, { + baseUrl: "https://gmail.googleapis.com", + path: "/gmail/v1/users/me/labels", + method: "POST", + body: req.body as JsonValue + }); +}); + +app.post("/google/docs/documents", async (req, res) => { + await callGoogle(req, res, { + baseUrl: "https://docs.googleapis.com", + path: "/v1/documents", + method: "POST", + body: req.body as JsonValue + }); +}); + +app.get("/google/docs/documents/:documentId", async (req, res) => { + await callGoogle(req, res, { + baseUrl: "https://docs.googleapis.com", + path: `/v1/documents/${encodePathParam(req.params.documentId)}` + }); +}); + +app.post("/google/docs/documents/:documentId/batch-update", async (req, res) => { + await callGoogle(req, res, { + baseUrl: "https://docs.googleapis.com", + path: `/v1/documents/${encodePathParam(req.params.documentId)}:batchUpdate`, + method: "POST", + body: req.body as JsonValue + }); +}); + +app.post("/google/slides/presentations", async (req, res) => { + await callGoogle(req, res, { + baseUrl: "https://slides.googleapis.com", + path: "/v1/presentations", + method: "POST", + body: req.body as JsonValue + }); +}); + +app.get("/google/slides/presentations/:presentationId", async (req, res) => { + await callGoogle(req, res, { + baseUrl: "https://slides.googleapis.com", + path: `/v1/presentations/${encodePathParam(req.params.presentationId)}` + }); +}); + +app.post("/google/slides/presentations/:presentationId/batch-update", async (req, res) => { + await callGoogle(req, res, { + baseUrl: "https://slides.googleapis.com", + path: `/v1/presentations/${encodePathParam(req.params.presentationId)}:batchUpdate`, + method: "POST", + body: req.body as JsonValue + }); +}); + +app.get("/google/forms/forms/:formId", async (req, res) => { + await callGoogle(req, res, { + baseUrl: "https://forms.googleapis.com", + path: `/v1/forms/${encodePathParam(req.params.formId)}` + }); +}); + +app.post("/google/forms/forms", async (req, res) => { + await callGoogle(req, res, { + baseUrl: "https://forms.googleapis.com", + path: "/v1/forms", + method: "POST", + body: req.body as JsonValue + }); +}); + +app.post("/google/forms/forms/:formId/batch-update", async (req, res) => { + await callGoogle(req, res, { + baseUrl: "https://forms.googleapis.com", + path: `/v1/forms/${encodePathParam(req.params.formId)}:batchUpdate`, + method: "POST", + body: req.body as JsonValue + }); +}); + +app.get("/google/forms/forms/:formId/responses", async (req, res) => { + await callGoogle(req, res, { + baseUrl: "https://forms.googleapis.com", + path: `/v1/forms/${encodePathParam(req.params.formId)}/responses` + }); +}); + +app.get("/google/people/connections", async (req, res) => { + await callGoogle(req, res, { baseUrl: "https://people.googleapis.com", path: "/v1/people/me/connections" }); +}); + +app.get("/google/people/:resourceName", async (req, res) => { + await callGoogle(req, res, { + baseUrl: "https://people.googleapis.com", + path: `/v1/people/${encodePathParam(req.params.resourceName)}` + }); +}); + +app.post("/google/people/contacts", async (req, res) => { + await callGoogle(req, res, { + baseUrl: "https://people.googleapis.com", + path: "/v1/people:createContact", + method: "POST", + body: req.body as JsonValue + }); +}); + +app.patch("/google/people/:resourceName", async (req, res) => { + await callGoogle(req, res, { + baseUrl: "https://people.googleapis.com", + path: `/v1/people/${encodePathParam(req.params.resourceName)}:updateContact`, + method: "PATCH", + body: req.body as JsonValue + }); +}); + +app.delete("/google/people/:resourceName", async (req, res) => { + await callGoogle(req, res, { + baseUrl: "https://people.googleapis.com", + path: `/v1/people/${encodePathParam(req.params.resourceName)}:deleteContact`, + method: "DELETE" + }); +}); + +app.get("/google/tasks/lists", async (req, res) => { + await callGoogle(req, res, { baseUrl: "https://tasks.googleapis.com", path: "/tasks/v1/users/@me/lists" }); +}); + +app.post("/google/tasks/lists", async (req, res) => { + await callGoogle(req, res, { + baseUrl: "https://tasks.googleapis.com", + path: "/tasks/v1/users/@me/lists", + method: "POST", + body: req.body as JsonValue + }); +}); + +app.get("/google/tasks/lists/:tasklistId/tasks", async (req, res) => { + await callGoogle(req, res, { + baseUrl: "https://tasks.googleapis.com", + path: `/tasks/v1/lists/${encodePathParam(req.params.tasklistId)}/tasks` + }); +}); + +app.post("/google/tasks/lists/:tasklistId/tasks", async (req, res) => { + await callGoogle(req, res, { + baseUrl: "https://tasks.googleapis.com", + path: `/tasks/v1/lists/${encodePathParam(req.params.tasklistId)}/tasks`, + method: "POST", + body: req.body as JsonValue + }); +}); + +app.patch("/google/tasks/lists/:tasklistId/tasks/:taskId", async (req, res) => { + await callGoogle(req, res, { + baseUrl: "https://tasks.googleapis.com", + path: `/tasks/v1/lists/${encodePathParam(req.params.tasklistId)}/tasks/${encodePathParam(req.params.taskId)}`, + method: "PATCH", + body: req.body as JsonValue + }); +}); + +app.delete("/google/tasks/lists/:tasklistId/tasks/:taskId", async (req, res) => { + await callGoogle(req, res, { + baseUrl: "https://tasks.googleapis.com", + path: `/tasks/v1/lists/${encodePathParam(req.params.tasklistId)}/tasks/${encodePathParam(req.params.taskId)}`, + method: "DELETE" + }); +}); + app.post("/google/request", async (req, res) => { try { const requestBody = req.body as GoogleRequestBody; @@ -305,6 +742,73 @@ function withRootPath(path: string): string { return rootPath + path; } +async function callGoogle(req: Request, res: Response, options: GoogleRouteOptions): Promise { + try { + const requestBody: GoogleRequestBody = { + baseUrl: options.baseUrl, + path: options.path, + method: options.method || req.method, + query: { ...queryToRecord(req.query), ...(options.query || {}) }, + body: options.body, + accessToken: readBodyString(req.body, "accessToken"), + accessTokenEnv: readBodyString(req.body, "accessTokenEnv"), + apiKey: readBodyString(req.body, "apiKey"), + apiKeyEnv: readBodyString(req.body, "apiKeyEnv") + }; + + const targetUrl = buildGoogleUrl(requestBody); + const headers = buildGoogleHeaders(req, requestBody); + const method = (requestBody.method || "GET").toUpperCase(); + const init: RequestInit = { method, headers }; + + if (!["GET", "HEAD"].includes(method) && options.body !== undefined) { + init.body = JSON.stringify(options.body); + } + + const response = await fetch(targetUrl, init); + await relayResponse(res, response); + } catch (error) { + sendError(res, error); + } +} + +function queryToRecord(query: Request["query"]): Record { + const result: Record = {}; + + for (const [key, value] of Object.entries(query)) { + if (typeof value === "string") { + result[key] = value; + } else if (Array.isArray(value)) { + result[key] = value.map((item) => String(item)).join(","); + } + } + + return result; +} + +function readBodyString(body: unknown, key: string): string | undefined { + if (!body || typeof body !== "object") { + return undefined; + } + + const value = (body as Record)[key]; + return typeof value === "string" ? value : undefined; +} + +function requireQueryString(req: Request, res: Response, key: string): string | undefined { + const value = req.query[key]; + if (typeof value === "string" && value) { + return value; + } + + badRequest(res, `Missing required query parameter: ${key}`); + return undefined; +} + +function encodePathParam(value: string): string { + return encodeURIComponent(value); +} + function buildGoogleUrl(body: GoogleRequestBody): string { const rawUrl = body.url || joinBaseAndPath(body.baseUrl || "https://www.googleapis.com", body.path || ""); const url = new URL(rawUrl); @@ -486,6 +990,15 @@ function buildOpenApiDocument(): Record { { name: "System" }, { name: "Google Discovery" }, { name: "Google OAuth" }, + { name: "Google Calendar" }, + { name: "Google Sheets" }, + { name: "Google Drive" }, + { name: "Gmail" }, + { name: "Google Docs" }, + { name: "Google Slides" }, + { name: "Google Forms" }, + { name: "Google People" }, + { name: "Google Tasks" }, { name: "Google API" } ], paths: { @@ -553,6 +1066,7 @@ function buildOpenApiDocument(): Record { responses: { "200": { description: "Token info" }, "400": { description: "Invalid request" } } } }, + ...buildGoogleServiceOpenApiPaths(), "/google/request": { post: { tags: ["Google API"], @@ -591,3 +1105,172 @@ function jsonRequestBody(example: Record): Record { + return { + "/google/calendar/calendars": { + get: googleOperation("Google Calendar", "List calendars"), + post: googleOperation("Google Calendar", "Create calendar", { summary: "Team calendar" }) + }, + "/google/calendar/calendars/{calendarId}": { + get: googleOperation("Google Calendar", "Get calendar", undefined, ["calendarId"]), + patch: googleOperation("Google Calendar", "Update calendar", { summary: "Updated calendar" }, ["calendarId"]), + delete: googleOperation("Google Calendar", "Delete calendar", undefined, ["calendarId"]) + }, + "/google/calendar/calendars/{calendarId}/events": { + get: googleOperation("Google Calendar", "List events", undefined, ["calendarId"]), + post: googleOperation("Google Calendar", "Create event", { summary: "Meeting", start: {}, end: {} }, ["calendarId"]) + }, + "/google/calendar/calendars/{calendarId}/events/{eventId}": { + get: googleOperation("Google Calendar", "Get event", undefined, ["calendarId", "eventId"]), + patch: googleOperation("Google Calendar", "Update event", { summary: "Updated meeting" }, ["calendarId", "eventId"]), + delete: googleOperation("Google Calendar", "Delete event", undefined, ["calendarId", "eventId"]) + }, + "/google/calendar/calendars/{calendarId}/events/{eventId}/move": { + post: googleOperation("Google Calendar", "Move event to another calendar", undefined, ["calendarId", "eventId"]) + }, + "/google/sheets/spreadsheets": { + post: googleOperation("Google Sheets", "Create spreadsheet", { properties: { title: "New spreadsheet" } }) + }, + "/google/sheets/spreadsheets/{spreadsheetId}": { + get: googleOperation("Google Sheets", "Get spreadsheet", undefined, ["spreadsheetId"]) + }, + "/google/sheets/spreadsheets/{spreadsheetId}/batch-update": { + post: googleOperation("Google Sheets", "Batch update spreadsheet", { requests: [] }, ["spreadsheetId"]) + }, + "/google/sheets/spreadsheets/{spreadsheetId}/values": { + get: googleOperation("Google Sheets", "Get values by range query parameter", undefined, ["spreadsheetId"], ["range"]), + put: googleOperation("Google Sheets", "Update values by range query parameter", { values: [["value"]] }, ["spreadsheetId"], ["range"]) + }, + "/google/sheets/spreadsheets/{spreadsheetId}/values/append": { + post: googleOperation("Google Sheets", "Append values by range query parameter", { values: [["value"]] }, ["spreadsheetId"], ["range"]) + }, + "/google/sheets/spreadsheets/{spreadsheetId}/values/batch-update": { + post: googleOperation("Google Sheets", "Batch update values", { data: [], valueInputOption: "USER_ENTERED" }, ["spreadsheetId"]) + }, + "/google/sheets/spreadsheets/{spreadsheetId}/values/batch-clear": { + post: googleOperation("Google Sheets", "Batch clear values", { ranges: [] }, ["spreadsheetId"]) + }, + "/google/drive/files": { + get: googleOperation("Google Drive", "List files"), + post: googleOperation("Google Drive", "Create file metadata", { name: "New file" }) + }, + "/google/drive/files/{fileId}": { + get: googleOperation("Google Drive", "Get file metadata", undefined, ["fileId"]), + patch: googleOperation("Google Drive", "Update file metadata", { name: "Renamed file" }, ["fileId"]), + delete: googleOperation("Google Drive", "Delete file", undefined, ["fileId"]) + }, + "/google/drive/files/{fileId}/export": { + get: googleOperation("Google Drive", "Export Google Workspace file", undefined, ["fileId"], ["mimeType"]) + }, + "/google/drive/files/{fileId}/permissions": { + get: googleOperation("Google Drive", "List file permissions", undefined, ["fileId"]), + post: googleOperation("Google Drive", "Create file permission", { type: "user", role: "reader", emailAddress: "user@example.com" }, ["fileId"]) + }, + "/google/drive/files/{fileId}/permissions/{permissionId}": { + delete: googleOperation("Google Drive", "Delete file permission", undefined, ["fileId", "permissionId"]) + }, + "/google/gmail/profile": { + get: googleOperation("Gmail", "Get Gmail profile") + }, + "/google/gmail/messages": { + get: googleOperation("Gmail", "List messages") + }, + "/google/gmail/messages/{messageId}": { + get: googleOperation("Gmail", "Get message", undefined, ["messageId"]) + }, + "/google/gmail/messages/send": { + post: googleOperation("Gmail", "Send message", { raw: "base64url-rfc2822-message" }) + }, + "/google/gmail/labels": { + get: googleOperation("Gmail", "List labels"), + post: googleOperation("Gmail", "Create label", { name: "Label name" }) + }, + "/google/docs/documents": { + post: googleOperation("Google Docs", "Create document", { title: "New document" }) + }, + "/google/docs/documents/{documentId}": { + get: googleOperation("Google Docs", "Get document", undefined, ["documentId"]) + }, + "/google/docs/documents/{documentId}/batch-update": { + post: googleOperation("Google Docs", "Batch update document", { requests: [] }, ["documentId"]) + }, + "/google/slides/presentations": { + post: googleOperation("Google Slides", "Create presentation", { title: "New presentation" }) + }, + "/google/slides/presentations/{presentationId}": { + get: googleOperation("Google Slides", "Get presentation", undefined, ["presentationId"]) + }, + "/google/slides/presentations/{presentationId}/batch-update": { + post: googleOperation("Google Slides", "Batch update presentation", { requests: [] }, ["presentationId"]) + }, + "/google/forms/forms": { + post: googleOperation("Google Forms", "Create form", { info: { title: "New form" } }) + }, + "/google/forms/forms/{formId}": { + get: googleOperation("Google Forms", "Get form", undefined, ["formId"]) + }, + "/google/forms/forms/{formId}/batch-update": { + post: googleOperation("Google Forms", "Batch update form", { requests: [] }, ["formId"]) + }, + "/google/forms/forms/{formId}/responses": { + get: googleOperation("Google Forms", "List form responses", undefined, ["formId"]) + }, + "/google/people/connections": { + get: googleOperation("Google People", "List connections") + }, + "/google/people/{resourceName}": { + get: googleOperation("Google People", "Get person/contact", undefined, ["resourceName"]), + patch: googleOperation("Google People", "Update contact", {}, ["resourceName"]), + delete: googleOperation("Google People", "Delete contact", undefined, ["resourceName"]) + }, + "/google/people/contacts": { + post: googleOperation("Google People", "Create contact", { names: [{ givenName: "Ada", familyName: "Lovelace" }] }) + }, + "/google/tasks/lists": { + get: googleOperation("Google Tasks", "List task lists"), + post: googleOperation("Google Tasks", "Create task list", { title: "Task list" }) + }, + "/google/tasks/lists/{tasklistId}/tasks": { + get: googleOperation("Google Tasks", "List tasks", undefined, ["tasklistId"]), + post: googleOperation("Google Tasks", "Create task", { title: "Task" }, ["tasklistId"]) + }, + "/google/tasks/lists/{tasklistId}/tasks/{taskId}": { + patch: googleOperation("Google Tasks", "Update task", { title: "Updated task" }, ["tasklistId", "taskId"]), + delete: googleOperation("Google Tasks", "Delete task", undefined, ["tasklistId", "taskId"]) + } + }; +} + +function googleOperation( + tag: string, + summary: string, + example?: Record, + pathParams: string[] = [], + queryParams: string[] = [] +): Record { + const parameters = [ + ...pathParams.map((name) => ({ name, in: "path", required: true, schema: { type: "string" } })), + ...queryParams.map((name) => ({ name, in: "query", required: true, schema: { type: "string" } })) + ]; + + const operation: Record = { + tags: [tag], + summary, + security: [{ bearerAuth: [] }], + responses: { + "200": { description: "Google API response" }, + "400": { description: "Invalid request or Google API error" } + } + }; + + if (parameters.length > 0) { + operation.parameters = parameters; + } + + if (example) { + operation.requestBody = jsonRequestBody(example); + } + + return operation; +}