upd
This commit is contained in:
@@ -11,11 +11,14 @@ Node.js TypeScript sluzba pro komunikaci s Google API za AppFactory reverse prox
|
||||
- `GET /google/discovery/apis`
|
||||
- `GET /google/discovery/apis/{api}/{version}/rest`
|
||||
- `POST /google/oauth/token`
|
||||
- `GET /google/oauth/scopes`
|
||||
- `GET /google/oauth/authorize-url`
|
||||
- `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/...`
|
||||
- `POST /google/sheets/write-by-url`
|
||||
- `GET|POST|PATCH|DELETE /google/drive/...`
|
||||
- `GET|POST /google/gmail/...`
|
||||
- `GET|POST /google/docs/...`
|
||||
@@ -37,11 +40,29 @@ Volitelne environment variables:
|
||||
|
||||
Secrets se nevraci v zadnem endpointu a neloguji se.
|
||||
|
||||
## Credentials ve Swaggeru
|
||||
|
||||
`GET /google/configuration` vraci prehled podporovanych env promennych a jejich request alternativ. Nevraci hodnoty secrets.
|
||||
|
||||
Env promenne lze nahradit primo v requestu:
|
||||
|
||||
- `GOOGLE_CLIENT_ID`: `clientId` nebo `X-Google-Client-Id`
|
||||
- `GOOGLE_CLIENT_SECRET`: `clientSecret` nebo `X-Google-Client-Secret`
|
||||
- `GOOGLE_REDIRECT_URI`: `redirectUri` nebo `X-Google-Redirect-Uri`
|
||||
- `GOOGLE_SERVICE_ACCOUNT_EMAIL`: `serviceAccountEmail` nebo `X-Google-Service-Account-Email`
|
||||
- `GOOGLE_PRIVATE_KEY`: `privateKey` nebo `X-Google-Private-Key`
|
||||
- `GOOGLE_SCOPES`: `scope`, `scopes` nebo `X-Google-Scopes`
|
||||
- `GOOGLE_ACCESS_TOKEN`: `Authorization: Bearer <token>`, `accessToken`, `accessTokenEnv`, `X-Google-Access-Token`, `X-Google-Access-Token-Env`
|
||||
- `GOOGLE_API_KEY`: `apiKey`, `apiKeyEnv`, `X-Google-Api-Key`, `X-Google-Api-Key-Env`
|
||||
|
||||
## Obecne volani Google API
|
||||
|
||||
`POST /google/request` umi volat libovolny HTTPS endpoint na Google domene. Autorizace muze jit pres:
|
||||
|
||||
- `Authorization: Bearer <token>` header na requestu do sluzby
|
||||
- `X-Google-Access-Token` header
|
||||
- `X-Google-Access-Token-Env` header, napr. `GOOGLE_ACCESS_TOKEN`
|
||||
- `X-Google-Api-Key` nebo `X-Google-Api-Key-Env` header
|
||||
- `accessToken` v body
|
||||
- `accessTokenEnv` v body, napr. `GOOGLE_ACCESS_TOKEN`
|
||||
- `GOOGLE_API_KEY`, `apiKey` nebo `apiKeyEnv` pro endpointy podporujici API key
|
||||
@@ -102,3 +123,51 @@ POST /google/sheets/spreadsheets/{spreadsheetId}/values/append?range=Sheet1!A1
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Priklad zapisu do Google Sheets podle URL a nazvu listu:
|
||||
|
||||
```json
|
||||
POST /google/sheets/write-by-url
|
||||
{
|
||||
"spreadsheetUrl": "https://docs.google.com/spreadsheets/d/1abcDEFghiJKLmnopQRstuVWXyz/edit#gid=0",
|
||||
"sheetName": "Objednavky",
|
||||
"startCell": "A1",
|
||||
"mode": "append",
|
||||
"valueInputOption": "USER_ENTERED",
|
||||
"values": [
|
||||
["Datum", "Zakaznik", "Castka"],
|
||||
["2026-06-15", "ACME", 1234]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`mode=append` prida radky pod existujici data. `mode=update` prepise bunky od `startCell`.
|
||||
|
||||
## OAuth postup
|
||||
|
||||
1. Zavolej `GET /google/oauth/scopes` a vyber scopes podle sluzeb.
|
||||
2. Zavolej `GET /google/oauth/authorize-url?redirectUri=<callback>&scopes=<scopes>`.
|
||||
3. Otevri `authorizationUrl` z odpovedi v prohlizeci.
|
||||
4. Google presmeruje na `redirectUri?code=...`.
|
||||
5. Vymen code pres `POST /google/oauth/token`.
|
||||
6. Access token posilej jako `Authorization: Bearer <access_token>`.
|
||||
|
||||
Priklad vymeny code:
|
||||
|
||||
```json
|
||||
{
|
||||
"grantType": "authorization_code",
|
||||
"code": "code-from-google-redirect",
|
||||
"redirectUri": "https://example.test/oauth/callback"
|
||||
}
|
||||
```
|
||||
|
||||
## Kde vzit ID
|
||||
|
||||
- `calendarId`: `GET /google/calendar/calendars`, hlavni kalendar lze volat jako `primary`.
|
||||
- `spreadsheetId`: `GET /google/sheets/spreadsheets`, hodnota je `files[].id`.
|
||||
- `sheetId`: `GET /google/sheets/spreadsheets/{spreadsheetId}/sheets`, hodnota je `sheets[].properties.sheetId`.
|
||||
- A1 range pro Sheets hodnoty: pouzij `sheets[].properties.title`, napr. `Sheet1!A1:B10`.
|
||||
- `fileId`: `GET /google/drive/files`, hodnota je `files[].id`.
|
||||
- `messageId`: `GET /google/gmail/messages`, hodnota je `messages[].id`.
|
||||
- `documentId`, `presentationId`: u Docs/Slides jde o ID Drive souboru.
|
||||
|
||||
@@ -12,6 +12,8 @@ Discovery endpointy slouzi k dohledani dostupnych resource, metod, schema a OAut
|
||||
## OAuth
|
||||
|
||||
- `POST /google/oauth/token` podporuje `authorization_code` a `refresh_token` grant.
|
||||
- `GET /google/oauth/scopes` vraci doporucene scopes pro podporovane sluzby.
|
||||
- `GET /google/oauth/authorize-url` vygeneruje Google consent URL.
|
||||
- `POST /google/oauth/service-account-token` podporuje service account JWT bearer flow.
|
||||
- `POST /google/oauth/revoke` revokuje token.
|
||||
- `GET /google/oauth/tokeninfo` vraci informace o access tokenu nebo ID tokenu.
|
||||
@@ -24,6 +26,61 @@ OAuth client a service account hodnoty je vhodne predavat pres environment varia
|
||||
- `GOOGLE_PRIVATE_KEY`
|
||||
- `GOOGLE_SCOPES`
|
||||
|
||||
### Env promenne a request alternativy
|
||||
|
||||
Swagger obsahuje `GET /google/configuration`, ktery vypise podporovane env promenne, jejich pouziti a alternativy v requestu. Endpoint nevraci hodnoty secrets.
|
||||
|
||||
- `GOOGLE_CLIENT_ID`: `clientId`, `X-Google-Client-Id`
|
||||
- `GOOGLE_CLIENT_SECRET`: `clientSecret`, `X-Google-Client-Secret`
|
||||
- `GOOGLE_REDIRECT_URI`: `redirectUri`, `X-Google-Redirect-Uri`
|
||||
- `GOOGLE_SERVICE_ACCOUNT_EMAIL`: `serviceAccountEmail`, `X-Google-Service-Account-Email`
|
||||
- `GOOGLE_PRIVATE_KEY`: `privateKey`, `X-Google-Private-Key`
|
||||
- `GOOGLE_SCOPES`: `scope`, `scopes`, `X-Google-Scopes`
|
||||
- `GOOGLE_ACCESS_TOKEN`: `Authorization: Bearer <token>`, `accessToken`, `accessTokenEnv`, `X-Google-Access-Token`, `X-Google-Access-Token-Env`
|
||||
- `GOOGLE_API_KEY`: `apiKey`, `apiKeyEnv`, `X-Google-Api-Key`, `X-Google-Api-Key-Env`
|
||||
|
||||
### OAuth krok za krokem
|
||||
|
||||
1. Zavolej `GET /google/oauth/scopes`.
|
||||
2. Vyber scopes, napr. Sheets + Drive metadata:
|
||||
|
||||
```text
|
||||
https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/drive.metadata.readonly
|
||||
```
|
||||
|
||||
3. Zavolej:
|
||||
|
||||
```text
|
||||
GET /google/oauth/authorize-url?redirectUri=https://example.test/oauth/callback&scopes=https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/drive.metadata.readonly
|
||||
```
|
||||
|
||||
4. Otevri `authorizationUrl` z odpovedi.
|
||||
5. Po souhlasu Google presmeruje na `redirectUri` s query parametrem `code`.
|
||||
6. Code vymen:
|
||||
|
||||
```json
|
||||
{
|
||||
"grantType": "authorization_code",
|
||||
"code": "code-from-google-redirect",
|
||||
"redirectUri": "https://example.test/oauth/callback"
|
||||
}
|
||||
```
|
||||
|
||||
7. `access_token` posilej do produktovych endpointu jako:
|
||||
|
||||
```text
|
||||
Authorization: Bearer <access_token>
|
||||
```
|
||||
|
||||
Pro dlouhodobe pouziti si uloz `refresh_token` mimo zdrojovy kod a obnovuj token pres:
|
||||
|
||||
```json
|
||||
{
|
||||
"grantType": "refresh_token",
|
||||
"refreshToken": "stored-refresh-token"
|
||||
}
|
||||
```
|
||||
|
||||
## Obecne REST volani
|
||||
|
||||
`POST /google/request` je genericka proxy pro Google REST API. Request obsahuje HTTP metodu, cilovou Google URL nebo kombinaci `baseUrl` a `path`, volitelne query parametry, body a autorizaci.
|
||||
@@ -64,8 +121,11 @@ Krome obecne proxy jsou dostupne konkretni wrappery nad nejbeznejsimi Google API
|
||||
|
||||
### Sheets
|
||||
|
||||
- `GET /google/sheets/spreadsheets` - seznam Google Sheets souboru; `spreadsheetId` je `files[].id`
|
||||
- `POST /google/sheets/spreadsheets`
|
||||
- `GET /google/sheets/spreadsheets/{spreadsheetId}`
|
||||
- `GET /google/sheets/spreadsheets/{spreadsheetId}/sheets` - seznam tabu/listu; numericke `sheetId` je `sheets[].properties.sheetId`, A1 range pouziva `sheets[].properties.title`
|
||||
- `POST /google/sheets/write-by-url` - zapis hodnot podle plne URL spreadsheetu a nazvu listu
|
||||
- `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`
|
||||
@@ -73,6 +133,31 @@ Krome obecne proxy jsou dostupne konkretni wrappery nad nejbeznejsimi Google API
|
||||
- `POST /google/sheets/spreadsheets/{spreadsheetId}/values/batch-update`
|
||||
- `POST /google/sheets/spreadsheets/{spreadsheetId}/values/batch-clear`
|
||||
|
||||
#### Zapis podle URL a nazvu listu
|
||||
|
||||
Endpoint `POST /google/sheets/write-by-url` je urceny pro bezny zapis bez rucni prace se `spreadsheetId` a A1 range.
|
||||
|
||||
```json
|
||||
{
|
||||
"spreadsheetUrl": "https://docs.google.com/spreadsheets/d/1abcDEFghiJKLmnopQRstuVWXyz/edit#gid=0",
|
||||
"sheetName": "Objednavky",
|
||||
"startCell": "A1",
|
||||
"mode": "append",
|
||||
"valueInputOption": "USER_ENTERED",
|
||||
"values": [
|
||||
["Datum", "Zakaznik", "Castka"],
|
||||
["2026-06-15", "ACME", 1234]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Chovani:
|
||||
|
||||
- `mode=append` vola Google Sheets append a prida radky pod existujici tabulku.
|
||||
- `mode=update` vola Google Sheets update a prepise bunky od `startCell`.
|
||||
- `sheetName` muze obsahovat mezery i apostrofy; sluzba ho sama escapuje do A1 range.
|
||||
- `spreadsheetUrl` muze byt bezna URL ve tvaru `https://docs.google.com/spreadsheets/d/{spreadsheetId}/edit#gid=0`.
|
||||
|
||||
### Drive
|
||||
|
||||
- `GET /google/drive/files`
|
||||
@@ -121,6 +206,18 @@ Krome obecne proxy jsou dostupne konkretni wrappery nad nejbeznejsimi Google API
|
||||
- `PATCH /google/tasks/lists/{tasklistId}/tasks/{taskId}`
|
||||
- `DELETE /google/tasks/lists/{tasklistId}/tasks/{taskId}`
|
||||
|
||||
## Kde vzit ID
|
||||
|
||||
- `calendarId`: `GET /google/calendar/calendars`, hlavni kalendar lze typicky volat jako `primary`.
|
||||
- `eventId`: `GET /google/calendar/calendars/{calendarId}/events`.
|
||||
- `spreadsheetId`: `GET /google/sheets/spreadsheets`, pole `files[].id`.
|
||||
- `sheetId`: `GET /google/sheets/spreadsheets/{spreadsheetId}/sheets`, pole `sheets[].properties.sheetId`.
|
||||
- `fileId`: `GET /google/drive/files`, pole `files[].id`.
|
||||
- `messageId`: `GET /google/gmail/messages`, pole `messages[].id`.
|
||||
- `documentId`: ID Google Docs souboru z Drive.
|
||||
- `presentationId`: ID Google Slides souboru z Drive.
|
||||
- `formId`: ID Google Forms souboru z Drive nebo odpoved z `POST /google/forms/forms`.
|
||||
|
||||
## AppFactory overeni
|
||||
|
||||
Po deploy over:
|
||||
|
||||
+730
-19
@@ -35,6 +35,29 @@ type ServiceAccountTokenBody = {
|
||||
privateKey?: string;
|
||||
};
|
||||
|
||||
type SheetWriteByUrlBody = {
|
||||
spreadsheetUrl?: string;
|
||||
sheetName?: string;
|
||||
values?: JsonValue[][];
|
||||
startCell?: string;
|
||||
mode?: "append" | "update";
|
||||
majorDimension?: "ROWS" | "COLUMNS";
|
||||
valueInputOption?: "RAW" | "USER_ENTERED";
|
||||
insertDataOption?: "OVERWRITE" | "INSERT_ROWS";
|
||||
};
|
||||
|
||||
type AuthorizationUrlQuery = {
|
||||
redirectUri?: string;
|
||||
scope?: string;
|
||||
scopes?: string;
|
||||
state?: string;
|
||||
accessType?: string;
|
||||
prompt?: string;
|
||||
loginHint?: string;
|
||||
includeGrantedScopes?: string;
|
||||
clientId?: string;
|
||||
};
|
||||
|
||||
type GoogleRouteOptions = {
|
||||
baseUrl: string;
|
||||
path: string;
|
||||
@@ -47,6 +70,18 @@ const app = express();
|
||||
const port = Number(process.env.PORT || 3000);
|
||||
const rootPath = normalizeRootPath(process.env.ROOT_PATH || "");
|
||||
|
||||
const recommendedScopes = {
|
||||
calendar: ["https://www.googleapis.com/auth/calendar"],
|
||||
sheets: ["https://www.googleapis.com/auth/spreadsheets", "https://www.googleapis.com/auth/drive.metadata.readonly"],
|
||||
drive: ["https://www.googleapis.com/auth/drive"],
|
||||
gmail: ["https://www.googleapis.com/auth/gmail.modify", "https://www.googleapis.com/auth/gmail.send"],
|
||||
docs: ["https://www.googleapis.com/auth/documents"],
|
||||
slides: ["https://www.googleapis.com/auth/presentations"],
|
||||
forms: ["https://www.googleapis.com/auth/forms.body", "https://www.googleapis.com/auth/forms.responses.readonly"],
|
||||
people: ["https://www.googleapis.com/auth/contacts"],
|
||||
tasks: ["https://www.googleapis.com/auth/tasks"]
|
||||
};
|
||||
|
||||
const googleHosts = new Set([
|
||||
"accounts.google.com",
|
||||
"androidpublisher.googleapis.com",
|
||||
@@ -161,8 +196,8 @@ app.get("/google/discovery/apis/:api/:version/rest", async (req, res) => {
|
||||
app.post("/google/oauth/token", async (req, res) => {
|
||||
try {
|
||||
const body = req.body as TokenExchangeBody;
|
||||
const clientId = body.clientId || process.env.GOOGLE_CLIENT_ID;
|
||||
const clientSecret = body.clientSecret || process.env.GOOGLE_CLIENT_SECRET;
|
||||
const clientId = body.clientId || req.header("x-google-client-id") || process.env.GOOGLE_CLIENT_ID;
|
||||
const clientSecret = body.clientSecret || req.header("x-google-client-secret") || process.env.GOOGLE_CLIENT_SECRET;
|
||||
const grantType = body.grantType || (body.refreshToken ? "refresh_token" : "authorization_code");
|
||||
|
||||
if (!clientId || !clientSecret) {
|
||||
@@ -203,10 +238,10 @@ app.post("/google/oauth/token", async (req, res) => {
|
||||
app.post("/google/oauth/service-account-token", async (req, res) => {
|
||||
try {
|
||||
const body = req.body as ServiceAccountTokenBody;
|
||||
const email = body.serviceAccountEmail || process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL;
|
||||
const rawPrivateKey = body.privateKey || process.env.GOOGLE_PRIVATE_KEY;
|
||||
const email = body.serviceAccountEmail || req.header("x-google-service-account-email") || process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL;
|
||||
const rawPrivateKey = body.privateKey || req.header("x-google-private-key") || process.env.GOOGLE_PRIVATE_KEY;
|
||||
const privateKey = rawPrivateKey?.replace(/\\n/g, "\n");
|
||||
const scope = body.scope || body.scopes?.join(" ") || process.env.GOOGLE_SCOPES;
|
||||
const scope = body.scope || body.scopes?.join(" ") || req.header("x-google-scopes") || process.env.GOOGLE_SCOPES;
|
||||
|
||||
if (!email || !privateKey || !scope) {
|
||||
return badRequest(
|
||||
@@ -269,6 +304,64 @@ app.get("/google/oauth/tokeninfo", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/google/oauth/scopes", (_req, res) => {
|
||||
res.json({
|
||||
recommendedScopes,
|
||||
all: Array.from(new Set(Object.values(recommendedScopes).flat()))
|
||||
});
|
||||
});
|
||||
|
||||
app.get("/google/configuration", (_req, res) => {
|
||||
res.json({
|
||||
environmentVariables: [
|
||||
{ name: "GOOGLE_CLIENT_ID", usedBy: ["/google/oauth/authorize-url", "/google/oauth/token"], alternatives: ["clientId", "X-Google-Client-Id"] },
|
||||
{ name: "GOOGLE_CLIENT_SECRET", usedBy: ["/google/oauth/token"], alternatives: ["clientSecret", "X-Google-Client-Secret"] },
|
||||
{ name: "GOOGLE_REDIRECT_URI", usedBy: ["/google/oauth/authorize-url"], alternatives: ["redirectUri", "X-Google-Redirect-Uri"] },
|
||||
{ name: "GOOGLE_SERVICE_ACCOUNT_EMAIL", usedBy: ["/google/oauth/service-account-token"], alternatives: ["serviceAccountEmail", "X-Google-Service-Account-Email"] },
|
||||
{ name: "GOOGLE_PRIVATE_KEY", usedBy: ["/google/oauth/service-account-token"], alternatives: ["privateKey", "X-Google-Private-Key"] },
|
||||
{ name: "GOOGLE_SCOPES", usedBy: ["/google/oauth/service-account-token"], alternatives: ["scope", "scopes", "X-Google-Scopes"] },
|
||||
{ name: "GOOGLE_ACCESS_TOKEN", usedBy: ["Google product endpoints", "/google/request"], alternatives: ["Authorization: Bearer", "accessToken", "accessTokenEnv", "X-Google-Access-Token", "X-Google-Access-Token-Env"] },
|
||||
{ name: "GOOGLE_API_KEY", usedBy: ["Google product endpoints", "/google/request"], alternatives: ["apiKey", "apiKeyEnv", "X-Google-Api-Key", "X-Google-Api-Key-Env"] }
|
||||
],
|
||||
note: "Endpoint intentionally returns only variable names and usage metadata, never secret values."
|
||||
});
|
||||
});
|
||||
|
||||
app.get("/google/oauth/authorize-url", (req, res) => {
|
||||
const query = req.query as AuthorizationUrlQuery;
|
||||
const clientId = query.clientId || req.header("x-google-client-id") || process.env.GOOGLE_CLIENT_ID;
|
||||
const redirectUri = query.redirectUri || req.header("x-google-redirect-uri") || process.env.GOOGLE_REDIRECT_URI;
|
||||
|
||||
if (!clientId || !redirectUri) {
|
||||
return badRequest(res, "Authorize URL requires GOOGLE_CLIENT_ID and GOOGLE_REDIRECT_URI or clientId and redirectUri query parameters.");
|
||||
}
|
||||
|
||||
const scopes = parseScopes(query.scope || query.scopes) || Object.values(recommendedScopes).flat();
|
||||
const url = new URL("https://accounts.google.com/o/oauth2/v2/auth");
|
||||
url.searchParams.set("client_id", clientId);
|
||||
url.searchParams.set("redirect_uri", redirectUri);
|
||||
url.searchParams.set("response_type", "code");
|
||||
url.searchParams.set("scope", scopes.join(" "));
|
||||
url.searchParams.set("access_type", query.accessType || "offline");
|
||||
url.searchParams.set("prompt", query.prompt || "consent");
|
||||
url.searchParams.set("include_granted_scopes", query.includeGrantedScopes || "true");
|
||||
|
||||
if (query.state) {
|
||||
url.searchParams.set("state", query.state);
|
||||
}
|
||||
|
||||
if (query.loginHint) {
|
||||
url.searchParams.set("login_hint", query.loginHint);
|
||||
}
|
||||
|
||||
res.json({
|
||||
authorizationUrl: url.toString(),
|
||||
clientId,
|
||||
redirectUri,
|
||||
scopes
|
||||
});
|
||||
});
|
||||
|
||||
app.get("/google/calendar/calendars", async (req, res) => {
|
||||
await callGoogle(req, res, { baseUrl: "https://www.googleapis.com", path: "/calendar/v3/users/me/calendarList" });
|
||||
});
|
||||
@@ -363,6 +456,17 @@ app.post("/google/sheets/spreadsheets", async (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
app.get("/google/sheets/spreadsheets", async (req, res) => {
|
||||
await callGoogle(req, res, {
|
||||
baseUrl: "https://www.googleapis.com",
|
||||
path: "/drive/v3/files",
|
||||
query: {
|
||||
q: "mimeType='application/vnd.google-apps.spreadsheet' and trashed=false",
|
||||
fields: "files(id,name,mimeType,webViewLink,createdTime,modifiedTime,owners(displayName,emailAddress)),nextPageToken"
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.get("/google/sheets/spreadsheets/:spreadsheetId", async (req, res) => {
|
||||
await callGoogle(req, res, {
|
||||
baseUrl: "https://sheets.googleapis.com",
|
||||
@@ -370,6 +474,63 @@ app.get("/google/sheets/spreadsheets/:spreadsheetId", async (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
app.get("/google/sheets/spreadsheets/:spreadsheetId/sheets", async (req, res) => {
|
||||
await callGoogle(req, res, {
|
||||
baseUrl: "https://sheets.googleapis.com",
|
||||
path: `/v4/spreadsheets/${encodePathParam(req.params.spreadsheetId)}`,
|
||||
query: {
|
||||
fields: "spreadsheetId,properties.title,sheets.properties"
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.post("/google/sheets/write-by-url", async (req, res) => {
|
||||
const body = req.body as SheetWriteByUrlBody;
|
||||
const spreadsheetUrl = body.spreadsheetUrl;
|
||||
const sheetName = body.sheetName;
|
||||
const values = body.values;
|
||||
|
||||
if (!spreadsheetUrl || !sheetName || !Array.isArray(values)) {
|
||||
return badRequest(res, "Body requires spreadsheetUrl, sheetName and values.");
|
||||
}
|
||||
|
||||
const spreadsheetId = extractSpreadsheetId(spreadsheetUrl);
|
||||
if (!spreadsheetId) {
|
||||
return badRequest(res, "spreadsheetUrl must contain /spreadsheets/d/{spreadsheetId} or id={spreadsheetId}.");
|
||||
}
|
||||
|
||||
const startCell = body.startCell || "A1";
|
||||
const range = `${quoteSheetName(sheetName)}!${startCell}`;
|
||||
const mode = body.mode || "append";
|
||||
const valueInputOption = body.valueInputOption || "USER_ENTERED";
|
||||
const requestBody = {
|
||||
majorDimension: body.majorDimension || "ROWS",
|
||||
values
|
||||
};
|
||||
|
||||
if (mode === "update") {
|
||||
await callGoogle(req, res, {
|
||||
baseUrl: "https://sheets.googleapis.com",
|
||||
path: `/v4/spreadsheets/${encodePathParam(spreadsheetId)}/values/${encodePathParam(range)}`,
|
||||
method: "PUT",
|
||||
query: { valueInputOption },
|
||||
body: requestBody
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await callGoogle(req, res, {
|
||||
baseUrl: "https://sheets.googleapis.com",
|
||||
path: `/v4/spreadsheets/${encodePathParam(spreadsheetId)}/values/${encodePathParam(range)}:append`,
|
||||
method: "POST",
|
||||
query: {
|
||||
valueInputOption,
|
||||
insertDataOption: body.insertDataOption || "INSERT_ROWS"
|
||||
},
|
||||
body: requestBody
|
||||
});
|
||||
});
|
||||
|
||||
app.post("/google/sheets/spreadsheets/:spreadsheetId/batch-update", async (req, res) => {
|
||||
await callGoogle(req, res, {
|
||||
baseUrl: "https://sheets.googleapis.com",
|
||||
@@ -701,6 +862,7 @@ app.delete("/google/tasks/lists/:tasklistId/tasks/:taskId", async (req, res) =>
|
||||
app.post("/google/request", async (req, res) => {
|
||||
try {
|
||||
const requestBody = req.body as GoogleRequestBody;
|
||||
applyCredentialHeaders(req, requestBody);
|
||||
const targetUrl = buildGoogleUrl(requestBody);
|
||||
const headers = buildGoogleHeaders(req, requestBody);
|
||||
const method = (requestBody.method || "GET").toUpperCase();
|
||||
@@ -756,6 +918,7 @@ async function callGoogle(req: Request, res: Response, options: GoogleRouteOptio
|
||||
apiKeyEnv: readBodyString(req.body, "apiKeyEnv")
|
||||
};
|
||||
|
||||
applyCredentialHeaders(req, requestBody);
|
||||
const targetUrl = buildGoogleUrl(requestBody);
|
||||
const headers = buildGoogleHeaders(req, requestBody);
|
||||
const method = (requestBody.method || "GET").toUpperCase();
|
||||
@@ -795,6 +958,13 @@ function readBodyString(body: unknown, key: string): string | undefined {
|
||||
return typeof value === "string" ? value : undefined;
|
||||
}
|
||||
|
||||
function applyCredentialHeaders(req: Request, body: GoogleRequestBody): void {
|
||||
body.accessToken ||= req.header("x-google-access-token");
|
||||
body.accessTokenEnv ||= req.header("x-google-access-token-env");
|
||||
body.apiKey ||= req.header("x-google-api-key");
|
||||
body.apiKeyEnv ||= req.header("x-google-api-key-env");
|
||||
}
|
||||
|
||||
function requireQueryString(req: Request, res: Response, key: string): string | undefined {
|
||||
const value = req.query[key];
|
||||
if (typeof value === "string" && value) {
|
||||
@@ -809,6 +979,36 @@ function encodePathParam(value: string): string {
|
||||
return encodeURIComponent(value);
|
||||
}
|
||||
|
||||
function extractSpreadsheetId(spreadsheetUrl: string): string | undefined {
|
||||
const fromPath = spreadsheetUrl.match(/\/spreadsheets\/d\/([a-zA-Z0-9-_]+)/);
|
||||
if (fromPath?.[1]) {
|
||||
return fromPath[1];
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(spreadsheetUrl);
|
||||
const id = url.searchParams.get("id");
|
||||
return id || undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function quoteSheetName(sheetName: string): string {
|
||||
return `'${sheetName.replace(/'/g, "''")}'`;
|
||||
}
|
||||
|
||||
function parseScopes(value: string | undefined): string[] | undefined {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return value
|
||||
.split(/[,\s]+/)
|
||||
.map((scope) => scope.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function buildGoogleUrl(body: GoogleRequestBody): string {
|
||||
const rawUrl = body.url || joinBaseAndPath(body.baseUrl || "https://www.googleapis.com", body.path || "");
|
||||
const url = new URL(rawUrl);
|
||||
@@ -983,7 +1183,8 @@ function buildOpenApiDocument(): Record<string, unknown> {
|
||||
info: {
|
||||
title: "google-service API",
|
||||
version: "1.0.0",
|
||||
description: "Obecna proxy a OAuth vrstva pro komunikaci s Google API."
|
||||
description:
|
||||
"Kompletni REST wrapper pro Google API. OAuth postup: 1) GET /google/oauth/scopes pro vyber scope, 2) GET /google/oauth/authorize-url pro URL souhlasu, 3) po redirectu vymen code pres POST /google/oauth/token, 4) access_token posilej jako Authorization: Bearer <token>. ID objektu ziskas list endpointy: spreadsheetId pres GET /google/sheets/spreadsheets, calendarId pres GET /google/calendar/calendars, fileId pres GET /google/drive/files."
|
||||
},
|
||||
servers: [{ url: rootPath || "/" }],
|
||||
tags: [
|
||||
@@ -1031,20 +1232,69 @@ function buildOpenApiDocument(): Record<string, unknown> {
|
||||
post: {
|
||||
tags: ["Google OAuth"],
|
||||
summary: "Vymena authorization code nebo refresh tokenu za access token",
|
||||
description:
|
||||
"Pro authorization_code nejdriv otevri authorizationUrl z /google/oauth/authorize-url. Google presmeruje na redirectUri s query parametrem code. Ten posli sem. Pro dalsi obnoveni tokenu pouzij refreshToken.",
|
||||
parameters: oauthClientHeaderParameters(),
|
||||
requestBody: jsonRequestBody({
|
||||
grantType: "authorization_code",
|
||||
code: "authorization-code",
|
||||
redirectUri: "https://example.test/oauth/callback"
|
||||
}),
|
||||
responses: { "200": { description: "OAuth token response" }, "400": { description: "Invalid request" } }
|
||||
redirectUri: "https://example.test/oauth/callback",
|
||||
clientId: "google-oauth-client-id.apps.googleusercontent.com",
|
||||
clientSecret: "google-oauth-client-secret"
|
||||
}, "OAuthTokenRequest"),
|
||||
responses: {
|
||||
"200": jsonResponse("OAuth token response", "OAuthTokenResponse"),
|
||||
"400": { description: "Invalid request" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/google/oauth/scopes": {
|
||||
get: {
|
||||
tags: ["Google OAuth"],
|
||||
summary: "Doporucene OAuth scopes pro podporovane Google sluzby",
|
||||
description: "Vraci scopes rozdelene podle Calendar, Sheets, Drive, Gmail, Docs, Slides, Forms, People a Tasks.",
|
||||
responses: { "200": jsonResponse("Recommended scopes", "ScopesResponse") }
|
||||
}
|
||||
},
|
||||
"/google/configuration": {
|
||||
get: {
|
||||
tags: ["Google OAuth"],
|
||||
summary: "Prehled podporovanych env promennych a request alternativ",
|
||||
description:
|
||||
"Vraci jen nazvy env promennych, kde se pouzivaji a jak je predat pres request body/header. Nikdy nevraci hodnoty secrets.",
|
||||
responses: { "200": jsonResponse("Google service configuration metadata", "ConfigurationResponse") }
|
||||
}
|
||||
},
|
||||
"/google/oauth/authorize-url": {
|
||||
get: {
|
||||
tags: ["Google OAuth"],
|
||||
summary: "Vygeneruje Google OAuth consent URL",
|
||||
description:
|
||||
"Otevri authorizationUrl v prohlizeci. Po souhlasu Google presmeruje na redirectUri s query parametrem code. Tento code vymen pres POST /google/oauth/token.",
|
||||
parameters: [
|
||||
{ name: "clientId", in: "query", required: false, schema: { type: "string" }, description: "Fallback je X-Google-Client-Id nebo GOOGLE_CLIENT_ID." },
|
||||
{ name: "redirectUri", in: "query", required: false, schema: { type: "string" }, description: "Fallback je GOOGLE_REDIRECT_URI." },
|
||||
{ name: "scopes", in: "query", required: false, schema: { type: "string" }, description: "Scopes oddelene mezerou nebo carkou. Pokud chybi, pouziji se vsechny doporucene scopes." },
|
||||
{ name: "state", in: "query", required: false, schema: { type: "string" } },
|
||||
{ name: "loginHint", in: "query", required: false, schema: { type: "string" } },
|
||||
...authorizeHeaderParameters()
|
||||
],
|
||||
responses: { "200": jsonResponse("Authorization URL", "AuthorizationUrlResponse"), "400": { description: "Invalid request" } }
|
||||
}
|
||||
},
|
||||
"/google/oauth/service-account-token": {
|
||||
post: {
|
||||
tags: ["Google OAuth"],
|
||||
summary: "Vystaveni access tokenu pro service account JWT flow",
|
||||
requestBody: jsonRequestBody({ scopes: ["https://www.googleapis.com/auth/cloud-platform"] }),
|
||||
responses: { "200": { description: "OAuth token response" }, "400": { description: "Invalid request" } }
|
||||
description:
|
||||
"Pouzij pro server-to-server komunikaci. Pro Workspace data uzivatelu je potreba domain-wide delegation a subject.",
|
||||
parameters: serviceAccountHeaderParameters(),
|
||||
requestBody: jsonRequestBody({
|
||||
serviceAccountEmail: "service-account@project.iam.gserviceaccount.com",
|
||||
privateKey: "-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n",
|
||||
scopes: ["https://www.googleapis.com/auth/cloud-platform"]
|
||||
}, "ServiceAccountTokenRequest"),
|
||||
responses: { "200": jsonResponse("OAuth token response", "OAuthTokenResponse"), "400": { description: "Invalid request" } }
|
||||
}
|
||||
},
|
||||
"/google/oauth/revoke": {
|
||||
@@ -1071,14 +1321,17 @@ function buildOpenApiDocument(): Record<string, unknown> {
|
||||
post: {
|
||||
tags: ["Google API"],
|
||||
summary: "Obecne volani Google REST API",
|
||||
description:
|
||||
"Credentials lze predat v body, pres X-Google-* headery, nebo jako env promennou uvedenou v *Env poli. Dostupne env fallbacky jsou GOOGLE_ACCESS_TOKEN a GOOGLE_API_KEY.",
|
||||
security: [{ bearerAuth: [] }],
|
||||
parameters: googleCredentialHeaderParameters(),
|
||||
requestBody: jsonRequestBody({
|
||||
method: "GET",
|
||||
baseUrl: "https://www.googleapis.com",
|
||||
path: "/drive/v3/files",
|
||||
query: { pageSize: 10 },
|
||||
accessTokenEnv: "GOOGLE_ACCESS_TOKEN"
|
||||
}),
|
||||
}, "GoogleRequest"),
|
||||
responses: { "200": { description: "Google API response" }, "400": { description: "Invalid request" } }
|
||||
}
|
||||
}
|
||||
@@ -1089,18 +1342,294 @@ function buildOpenApiDocument(): Record<string, unknown> {
|
||||
type: "http",
|
||||
scheme: "bearer"
|
||||
}
|
||||
},
|
||||
schemas: buildOpenApiSchemas()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function jsonRequestBody(example: Record<string, unknown>, schemaName?: string): Record<string, unknown> {
|
||||
return {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: schemaName ? { $ref: `#/components/schemas/${schemaName}` } : { type: "object", additionalProperties: true },
|
||||
example
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function jsonRequestBody(example: Record<string, unknown>): Record<string, unknown> {
|
||||
function jsonResponse(description: string, schemaName: string): Record<string, unknown> {
|
||||
return {
|
||||
required: true,
|
||||
description,
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { type: "object", additionalProperties: true },
|
||||
example
|
||||
schema: { $ref: `#/components/schemas/${schemaName}` }
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function buildOpenApiSchemas(): Record<string, unknown> {
|
||||
return {
|
||||
OAuthTokenRequest: {
|
||||
type: "object",
|
||||
properties: {
|
||||
grantType: { type: "string", enum: ["authorization_code", "refresh_token"], default: "authorization_code" },
|
||||
code: { type: "string", description: "Code z Google redirectu pro authorization_code flow." },
|
||||
refreshToken: { type: "string", description: "Refresh token pro refresh_token flow." },
|
||||
redirectUri: { type: "string", description: "Fallback/alternativa: GOOGLE_REDIRECT_URI se pouziva jen pro authorize-url, token endpoint potrebuje redirectUri v body pro authorization_code." },
|
||||
scope: { type: "string" },
|
||||
clientId: { type: "string", description: "Alternativa k X-Google-Client-Id nebo env GOOGLE_CLIENT_ID." },
|
||||
clientSecret: { type: "string", description: "Alternativa k X-Google-Client-Secret nebo env GOOGLE_CLIENT_SECRET." }
|
||||
}
|
||||
},
|
||||
OAuthTokenResponse: {
|
||||
type: "object",
|
||||
properties: {
|
||||
access_token: { type: "string", description: "Bearer token pro volani Google API." },
|
||||
expires_in: { type: "integer", example: 3599 },
|
||||
refresh_token: { type: "string", description: "Vraci se typicky pri prvnim consentu s access_type=offline." },
|
||||
scope: { type: "string" },
|
||||
token_type: { type: "string", example: "Bearer" },
|
||||
id_token: { type: "string" }
|
||||
}
|
||||
},
|
||||
AuthorizationUrlResponse: {
|
||||
type: "object",
|
||||
properties: {
|
||||
authorizationUrl: { type: "string", description: "URL otevri v prohlizeci pro Google consent." },
|
||||
clientId: { type: "string" },
|
||||
redirectUri: { type: "string" },
|
||||
scopes: { type: "array", items: { type: "string" } }
|
||||
}
|
||||
},
|
||||
ScopesResponse: {
|
||||
type: "object",
|
||||
properties: {
|
||||
recommendedScopes: { type: "object", additionalProperties: { type: "array", items: { type: "string" } } },
|
||||
all: { type: "array", items: { type: "string" } }
|
||||
}
|
||||
},
|
||||
ConfigurationResponse: {
|
||||
type: "object",
|
||||
properties: {
|
||||
environmentVariables: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string", example: "GOOGLE_CLIENT_ID" },
|
||||
usedBy: { type: "array", items: { type: "string" } },
|
||||
alternatives: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "Request body fields, query fields, or X-Google-* headers usable instead of env."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
note: { type: "string" }
|
||||
}
|
||||
},
|
||||
ServiceAccountTokenRequest: {
|
||||
type: "object",
|
||||
properties: {
|
||||
serviceAccountEmail: { type: "string", description: "Alternativa k X-Google-Service-Account-Email nebo env GOOGLE_SERVICE_ACCOUNT_EMAIL." },
|
||||
privateKey: { type: "string", description: "Alternativa k X-Google-Private-Key nebo env GOOGLE_PRIVATE_KEY. Podporuje \\n escapovani." },
|
||||
scopes: { type: "array", items: { type: "string" }, description: "Alternativa k X-Google-Scopes nebo env GOOGLE_SCOPES." },
|
||||
scope: { type: "string" },
|
||||
subject: { type: "string", description: "Workspace user pro domain-wide delegation." }
|
||||
}
|
||||
},
|
||||
GoogleRequest: {
|
||||
type: "object",
|
||||
properties: {
|
||||
method: { type: "string", example: "GET" },
|
||||
url: { type: "string", description: "Plna Google API URL. Alternativa k baseUrl+path." },
|
||||
baseUrl: { type: "string", example: "https://www.googleapis.com" },
|
||||
path: { type: "string", example: "/drive/v3/files" },
|
||||
query: { type: "object", additionalProperties: true },
|
||||
headers: { type: "object", additionalProperties: { type: "string" } },
|
||||
body: { type: "object", additionalProperties: true },
|
||||
accessToken: { type: "string", description: "Alternativa k Authorization Bearer nebo X-Google-Access-Token." },
|
||||
accessTokenEnv: { type: "string", example: "GOOGLE_ACCESS_TOKEN", description: "Jmeno env promenne s access tokenem." },
|
||||
apiKey: { type: "string", description: "Alternativa k X-Google-Api-Key nebo GOOGLE_API_KEY." },
|
||||
apiKeyEnv: { type: "string", example: "GOOGLE_API_KEY", description: "Jmeno env promenne s API key." }
|
||||
}
|
||||
},
|
||||
Calendar: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "calendarId pouzitelne v /google/calendar/calendars/{calendarId}/events." },
|
||||
summary: { type: "string" },
|
||||
description: { type: "string" },
|
||||
timeZone: { type: "string", example: "Europe/Prague" }
|
||||
}
|
||||
},
|
||||
CalendarEvent: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "eventId." },
|
||||
summary: { type: "string" },
|
||||
description: { type: "string" },
|
||||
location: { type: "string" },
|
||||
start: { $ref: "#/components/schemas/EventDateTime" },
|
||||
end: { $ref: "#/components/schemas/EventDateTime" },
|
||||
attendees: { type: "array", items: { type: "object", properties: { email: { type: "string" } } } }
|
||||
}
|
||||
},
|
||||
EventDateTime: {
|
||||
type: "object",
|
||||
properties: {
|
||||
dateTime: { type: "string", example: "2026-06-15T10:00:00+02:00" },
|
||||
date: { type: "string", example: "2026-06-15" },
|
||||
timeZone: { type: "string", example: "Europe/Prague" }
|
||||
}
|
||||
},
|
||||
DriveFile: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "fileId. U Google Sheets souboru je to zaroven spreadsheetId." },
|
||||
name: { type: "string" },
|
||||
mimeType: { type: "string" },
|
||||
webViewLink: { type: "string" },
|
||||
createdTime: { type: "string" },
|
||||
modifiedTime: { type: "string" }
|
||||
}
|
||||
},
|
||||
DriveFileList: {
|
||||
type: "object",
|
||||
properties: {
|
||||
files: { type: "array", items: { $ref: "#/components/schemas/DriveFile" } },
|
||||
nextPageToken: { type: "string" }
|
||||
}
|
||||
},
|
||||
Spreadsheet: {
|
||||
type: "object",
|
||||
properties: {
|
||||
spreadsheetId: { type: "string", description: "ID spreadsheetu pro vsechny /google/sheets/spreadsheets/{spreadsheetId} endpointy." },
|
||||
spreadsheetUrl: { type: "string" },
|
||||
properties: { type: "object", properties: { title: { type: "string" } } },
|
||||
sheets: { type: "array", items: { $ref: "#/components/schemas/Sheet" } }
|
||||
}
|
||||
},
|
||||
Sheet: {
|
||||
type: "object",
|
||||
properties: {
|
||||
properties: {
|
||||
type: "object",
|
||||
properties: {
|
||||
sheetId: { type: "integer", description: "Numericke ID tabu/listu pro batchUpdate requesty." },
|
||||
title: { type: "string", description: "Nazev listu pouzitelny v A1 range, napr. Sheet1!A1:B10." },
|
||||
index: { type: "integer" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
ValueRange: {
|
||||
type: "object",
|
||||
properties: {
|
||||
range: { type: "string", example: "Sheet1!A1:C3" },
|
||||
majorDimension: { type: "string", example: "ROWS" },
|
||||
values: { type: "array", items: { type: "array", items: { type: "string" } } }
|
||||
}
|
||||
},
|
||||
BatchUpdateValuesRequest: {
|
||||
type: "object",
|
||||
properties: {
|
||||
valueInputOption: { type: "string", example: "USER_ENTERED" },
|
||||
data: { type: "array", items: { $ref: "#/components/schemas/ValueRange" } }
|
||||
}
|
||||
},
|
||||
SheetWriteByUrlRequest: {
|
||||
type: "object",
|
||||
required: ["spreadsheetUrl", "sheetName", "values"],
|
||||
properties: {
|
||||
spreadsheetUrl: {
|
||||
type: "string",
|
||||
description: "Plna URL Google Sheets, napr. https://docs.google.com/spreadsheets/d/{spreadsheetId}/edit#gid=0."
|
||||
},
|
||||
sheetName: {
|
||||
type: "string",
|
||||
description: "Nazev listu/tabu ve spreadsheetu. Sluzba sama slozi A1 range a spravne escapuje mezery i apostrofy."
|
||||
},
|
||||
startCell: { type: "string", default: "A1", example: "A1" },
|
||||
mode: { type: "string", enum: ["append", "update"], default: "append" },
|
||||
majorDimension: { type: "string", enum: ["ROWS", "COLUMNS"], default: "ROWS" },
|
||||
valueInputOption: { type: "string", enum: ["RAW", "USER_ENTERED"], default: "USER_ENTERED" },
|
||||
insertDataOption: { type: "string", enum: ["OVERWRITE", "INSERT_ROWS"], default: "INSERT_ROWS" },
|
||||
values: {
|
||||
type: "array",
|
||||
items: { type: "array", items: { oneOf: [{ type: "string" }, { type: "number" }, { type: "boolean" }] } },
|
||||
example: [["Datum", "Castka"], ["2026-06-15", 1234]]
|
||||
},
|
||||
accessToken: { type: "string", description: "Volitelne. Lepsi je poslat Authorization: Bearer <token>." },
|
||||
accessTokenEnv: { type: "string", description: "Volitelne jmeno env var s access tokenem." }
|
||||
}
|
||||
},
|
||||
SheetWriteResponse: {
|
||||
type: "object",
|
||||
properties: {
|
||||
spreadsheetId: { type: "string" },
|
||||
tableRange: { type: "string" },
|
||||
updatedRange: { type: "string" },
|
||||
updatedRows: { type: "integer" },
|
||||
updatedColumns: { type: "integer" },
|
||||
updatedCells: { type: "integer" },
|
||||
updates: { type: "object" }
|
||||
}
|
||||
},
|
||||
GmailMessage: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "messageId." },
|
||||
threadId: { type: "string" },
|
||||
labelIds: { type: "array", items: { type: "string" } },
|
||||
snippet: { type: "string" },
|
||||
raw: { type: "string", description: "Base64url encoded RFC 2822 message, pouziva se pro send." }
|
||||
}
|
||||
},
|
||||
GoogleDocument: {
|
||||
type: "object",
|
||||
properties: {
|
||||
documentId: { type: "string" },
|
||||
title: { type: "string" },
|
||||
body: { type: "object" }
|
||||
}
|
||||
},
|
||||
Presentation: {
|
||||
type: "object",
|
||||
properties: {
|
||||
presentationId: { type: "string" },
|
||||
title: { type: "string" },
|
||||
slides: { type: "array", items: { type: "object" } }
|
||||
}
|
||||
},
|
||||
GoogleForm: {
|
||||
type: "object",
|
||||
properties: {
|
||||
formId: { type: "string" },
|
||||
info: { type: "object", properties: { title: { type: "string" } } }
|
||||
}
|
||||
},
|
||||
Person: {
|
||||
type: "object",
|
||||
properties: {
|
||||
resourceName: { type: "string", description: "Identifikator kontaktu pro People API." },
|
||||
names: { type: "array", items: { type: "object" } },
|
||||
emailAddresses: { type: "array", items: { type: "object" } }
|
||||
}
|
||||
},
|
||||
Task: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "taskId." },
|
||||
title: { type: "string" },
|
||||
notes: { type: "string" },
|
||||
status: { type: "string" },
|
||||
due: { type: "string" }
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1130,11 +1659,43 @@ function buildGoogleServiceOpenApiPaths(): Record<string, unknown> {
|
||||
post: googleOperation("Google Calendar", "Move event to another calendar", undefined, ["calendarId", "eventId"])
|
||||
},
|
||||
"/google/sheets/spreadsheets": {
|
||||
get: googleOperation("Google Sheets", "List spreadsheet files. spreadsheetId is files[].id in the response"),
|
||||
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}/sheets": {
|
||||
get: googleOperation("Google Sheets", "List sheets/tabs. sheetId is sheets[].properties.sheetId and A1 ranges use sheets[].properties.title", undefined, ["spreadsheetId"])
|
||||
},
|
||||
"/google/sheets/write-by-url": {
|
||||
post: {
|
||||
tags: ["Google Sheets"],
|
||||
summary: "Write values by spreadsheet URL and sheet name",
|
||||
description:
|
||||
"Nejjednodussi zapis do Google Sheets. Posli plnou spreadsheetUrl, sheetName a values. Sluzba sama vytahne spreadsheetId z URL a slozi A1 range. mode=append prida radky pod existujici tabulku, mode=update prepise hodnoty od startCell.",
|
||||
security: [{ bearerAuth: [] }],
|
||||
parameters: googleCredentialHeaderParameters(),
|
||||
requestBody: jsonRequestBody(
|
||||
{
|
||||
spreadsheetUrl: "https://docs.google.com/spreadsheets/d/1abcDEFghiJKLmnopQRstuVWXyz/edit#gid=0",
|
||||
sheetName: "Objednavky",
|
||||
startCell: "A1",
|
||||
mode: "append",
|
||||
valueInputOption: "USER_ENTERED",
|
||||
values: [
|
||||
["Datum", "Zakaznik", "Castka"],
|
||||
["2026-06-15", "ACME", 1234]
|
||||
]
|
||||
},
|
||||
"SheetWriteByUrlRequest"
|
||||
),
|
||||
responses: {
|
||||
"200": jsonResponse("Google Sheets write response", "SheetWriteResponse"),
|
||||
"400": { description: "Invalid request or Google API error" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/google/sheets/spreadsheets/{spreadsheetId}/batch-update": {
|
||||
post: googleOperation("Google Sheets", "Batch update spreadsheet", { requests: [] }, ["spreadsheetId"])
|
||||
},
|
||||
@@ -1242,6 +1803,103 @@ function buildGoogleServiceOpenApiPaths(): Record<string, unknown> {
|
||||
};
|
||||
}
|
||||
|
||||
function googleCredentialHeaderParameters(): Record<string, unknown>[] {
|
||||
return [
|
||||
{
|
||||
name: "X-Google-Access-Token",
|
||||
in: "header",
|
||||
required: false,
|
||||
schema: { type: "string" },
|
||||
description: "Volitelny Google OAuth access token. Alternativy: Authorization: Bearer <token>, body.accessToken, nebo env pres X-Google-Access-Token-Env/body.accessTokenEnv."
|
||||
},
|
||||
{
|
||||
name: "X-Google-Access-Token-Env",
|
||||
in: "header",
|
||||
required: false,
|
||||
schema: { type: "string", example: "GOOGLE_ACCESS_TOKEN" },
|
||||
description: "Jmeno env promenne obsahujici access token. Hodnota env se nevraci ve vystupu."
|
||||
},
|
||||
{
|
||||
name: "X-Google-Api-Key",
|
||||
in: "header",
|
||||
required: false,
|
||||
schema: { type: "string" },
|
||||
description: "Volitelny Google API key pro endpointy, ktere API key podporuji. Alternativy: body.apiKey nebo env GOOGLE_API_KEY / X-Google-Api-Key-Env."
|
||||
},
|
||||
{
|
||||
name: "X-Google-Api-Key-Env",
|
||||
in: "header",
|
||||
required: false,
|
||||
schema: { type: "string", example: "GOOGLE_API_KEY" },
|
||||
description: "Jmeno env promenne obsahujici Google API key. Hodnota env se nevraci ve vystupu."
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
function oauthClientHeaderParameters(): Record<string, unknown>[] {
|
||||
return [
|
||||
{
|
||||
name: "X-Google-Client-Id",
|
||||
in: "header",
|
||||
required: false,
|
||||
schema: { type: "string" },
|
||||
description: "Alternativa k body.clientId nebo env GOOGLE_CLIENT_ID."
|
||||
},
|
||||
{
|
||||
name: "X-Google-Client-Secret",
|
||||
in: "header",
|
||||
required: false,
|
||||
schema: { type: "string" },
|
||||
description: "Alternativa k body.clientSecret nebo env GOOGLE_CLIENT_SECRET. Secret se nevraci ve vystupu."
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
function authorizeHeaderParameters(): Record<string, unknown>[] {
|
||||
return [
|
||||
{
|
||||
name: "X-Google-Client-Id",
|
||||
in: "header",
|
||||
required: false,
|
||||
schema: { type: "string" },
|
||||
description: "Alternativa k query clientId nebo env GOOGLE_CLIENT_ID."
|
||||
},
|
||||
{
|
||||
name: "X-Google-Redirect-Uri",
|
||||
in: "header",
|
||||
required: false,
|
||||
schema: { type: "string" },
|
||||
description: "Alternativa k query redirectUri nebo env GOOGLE_REDIRECT_URI."
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
function serviceAccountHeaderParameters(): Record<string, unknown>[] {
|
||||
return [
|
||||
{
|
||||
name: "X-Google-Service-Account-Email",
|
||||
in: "header",
|
||||
required: false,
|
||||
schema: { type: "string" },
|
||||
description: "Alternativa k body.serviceAccountEmail nebo env GOOGLE_SERVICE_ACCOUNT_EMAIL."
|
||||
},
|
||||
{
|
||||
name: "X-Google-Private-Key",
|
||||
in: "header",
|
||||
required: false,
|
||||
schema: { type: "string" },
|
||||
description: "Alternativa k body.privateKey nebo env GOOGLE_PRIVATE_KEY. Secret se nevraci ve vystupu."
|
||||
},
|
||||
{
|
||||
name: "X-Google-Scopes",
|
||||
in: "header",
|
||||
required: false,
|
||||
schema: { type: "string" },
|
||||
description: "Alternativa k body.scope/body.scopes nebo env GOOGLE_SCOPES."
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
function googleOperation(
|
||||
tag: string,
|
||||
summary: string,
|
||||
@@ -1251,15 +1909,17 @@ function googleOperation(
|
||||
): 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" } }))
|
||||
...queryParams.map((name) => ({ name, in: "query", required: true, schema: { type: "string" } })),
|
||||
...googleCredentialHeaderParameters()
|
||||
];
|
||||
|
||||
const operation: Record<string, unknown> = {
|
||||
tags: [tag],
|
||||
summary,
|
||||
description: descriptionForOperation(tag, summary),
|
||||
security: [{ bearerAuth: [] }],
|
||||
responses: {
|
||||
"200": { description: "Google API response" },
|
||||
"200": jsonResponse("Google API response", responseSchemaForOperation(tag, summary)),
|
||||
"400": { description: "Invalid request or Google API error" }
|
||||
}
|
||||
};
|
||||
@@ -1269,8 +1929,59 @@ function googleOperation(
|
||||
}
|
||||
|
||||
if (example) {
|
||||
operation.requestBody = jsonRequestBody(example);
|
||||
operation.requestBody = jsonRequestBody(example, requestSchemaForOperation(tag, summary));
|
||||
}
|
||||
|
||||
return operation;
|
||||
}
|
||||
|
||||
function responseSchemaForOperation(tag: string, summary: string): string {
|
||||
if (tag === "Google Sheets" && summary.includes("spreadsheetId is files")) return "DriveFileList";
|
||||
if (tag === "Google Calendar" && summary.includes("event")) return "CalendarEvent";
|
||||
if (tag === "Google Calendar") return "Calendar";
|
||||
if (tag === "Google Sheets" && summary.includes("values")) return "ValueRange";
|
||||
if (tag === "Google Sheets") return "Spreadsheet";
|
||||
if (tag === "Google Drive" || tag === "Google Sheets" && summary.includes("List")) return "DriveFileList";
|
||||
if (tag === "Gmail") return "GmailMessage";
|
||||
if (tag === "Google Docs") return "GoogleDocument";
|
||||
if (tag === "Google Slides") return "Presentation";
|
||||
if (tag === "Google Forms") return "GoogleForm";
|
||||
if (tag === "Google People") return "Person";
|
||||
if (tag === "Google Tasks") return "Task";
|
||||
return "DriveFileList";
|
||||
}
|
||||
|
||||
function requestSchemaForOperation(tag: string, summary: string): string | undefined {
|
||||
if (tag === "Google Calendar" && summary.includes("event")) return "CalendarEvent";
|
||||
if (tag === "Google Calendar") return "Calendar";
|
||||
if (tag === "Google Sheets" && summary.includes("values")) return summary.includes("Batch") ? "BatchUpdateValuesRequest" : "ValueRange";
|
||||
if (tag === "Google Sheets") return "Spreadsheet";
|
||||
if (tag === "Google Drive") return "DriveFile";
|
||||
if (tag === "Gmail") return "GmailMessage";
|
||||
if (tag === "Google Docs") return "GoogleDocument";
|
||||
if (tag === "Google Slides") return "Presentation";
|
||||
if (tag === "Google Forms") return "GoogleForm";
|
||||
if (tag === "Google People") return "Person";
|
||||
if (tag === "Google Tasks") return "Task";
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function descriptionForOperation(tag: string, summary: string): string {
|
||||
if (summary.includes("spreadsheetId is files")) {
|
||||
return "Pouzij tento endpoint pro ziskani spreadsheetId. V odpovedi je spreadsheetId v poli files[].id. Stejna hodnota je ID Google Drive souboru.";
|
||||
}
|
||||
|
||||
if (summary.includes("sheetId is sheets")) {
|
||||
return "Pouzij tento endpoint pro ziskani tabu/listu ve spreadsheetu. Numericky sheetId je sheets[].properties.sheetId. Pro values endpointy pouzij A1 range z title, napr. Sheet1!A1:B10.";
|
||||
}
|
||||
|
||||
if (tag === "Google Calendar" && summary.includes("List calendars")) {
|
||||
return "Pouzij pro ziskani calendarId. Hlavni kalendar lze typicky volat jako calendarId=primary, ostatni ID jsou v id.";
|
||||
}
|
||||
|
||||
if (tag === "Google Drive" && summary.includes("List files")) {
|
||||
return "Pouzij pro ziskani fileId pro Drive/Docs/Sheets/Slides soubory. Google Sheets spreadsheetId odpovida Drive file id.";
|
||||
}
|
||||
|
||||
return "Volej s Authorization: Bearer <access_token>. Token ziskas pres /google/oauth/authorize-url a /google/oauth/token, nebo service account flow.";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user