rozsireni

This commit is contained in:
JiriUhlir
2026-06-15 15:19:01 +02:00
parent a882456466
commit 54247186cf
3 changed files with 812 additions and 0 deletions
+52
View File
@@ -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/service-account-token`
- `POST /google/oauth/revoke` - `POST /google/oauth/revoke`
- `GET /google/oauth/tokeninfo` - `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` - `POST /google/request`
## Konfigurace ## Konfigurace
@@ -50,3 +59,46 @@ Priklad:
"accessTokenEnv": "GOOGLE_ACCESS_TOKEN" "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 <token>`, 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"]
]
}
```
+77
View File
@@ -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. 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 ## AppFactory overeni
Po deploy over: Po deploy over:
+683
View File
@@ -35,6 +35,14 @@ type ServiceAccountTokenBody = {
privateKey?: string; privateKey?: string;
}; };
type GoogleRouteOptions = {
baseUrl: string;
path: string;
method?: string;
query?: Record<string, string | number | boolean | null | undefined>;
body?: JsonValue;
};
const app = express(); const app = express();
const port = Number(process.env.PORT || 3000); const port = Number(process.env.PORT || 3000);
const rootPath = normalizeRootPath(process.env.ROOT_PATH || ""); 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) => { app.post("/google/request", async (req, res) => {
try { try {
const requestBody = req.body as GoogleRequestBody; const requestBody = req.body as GoogleRequestBody;
@@ -305,6 +742,73 @@ function withRootPath(path: string): string {
return rootPath + path; return rootPath + path;
} }
async function callGoogle(req: Request, res: Response, options: GoogleRouteOptions): Promise<void> {
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<string, string | number | boolean | null | undefined> {
const result: Record<string, string | number | boolean | null | undefined> = {};
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<string, unknown>)[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 { function buildGoogleUrl(body: GoogleRequestBody): string {
const rawUrl = body.url || joinBaseAndPath(body.baseUrl || "https://www.googleapis.com", body.path || ""); const rawUrl = body.url || joinBaseAndPath(body.baseUrl || "https://www.googleapis.com", body.path || "");
const url = new URL(rawUrl); const url = new URL(rawUrl);
@@ -486,6 +990,15 @@ function buildOpenApiDocument(): Record<string, unknown> {
{ name: "System" }, { name: "System" },
{ name: "Google Discovery" }, { name: "Google Discovery" },
{ name: "Google OAuth" }, { 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" } { name: "Google API" }
], ],
paths: { paths: {
@@ -553,6 +1066,7 @@ function buildOpenApiDocument(): Record<string, unknown> {
responses: { "200": { description: "Token info" }, "400": { description: "Invalid request" } } responses: { "200": { description: "Token info" }, "400": { description: "Invalid request" } }
} }
}, },
...buildGoogleServiceOpenApiPaths(),
"/google/request": { "/google/request": {
post: { post: {
tags: ["Google API"], tags: ["Google API"],
@@ -591,3 +1105,172 @@ function jsonRequestBody(example: Record<string, unknown>): Record<string, unkno
} }
}; };
} }
function buildGoogleServiceOpenApiPaths(): Record<string, unknown> {
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<string, unknown>,
pathParams: string[] = [],
queryParams: string[] = []
): Record<string, unknown> {
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<string, unknown> = {
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;
}