google-apis-cli 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +39 -26
  2. package/dist/bin/gapi.js +6 -137
  3. package/dist/src/apis/ads.js +51 -49
  4. package/dist/src/apis/bid-manager.js +4 -0
  5. package/dist/src/apis/bigquery-dts.js +4 -0
  6. package/dist/src/apis/business-account-management.js +5 -0
  7. package/dist/src/apis/business-information.js +5 -0
  8. package/dist/src/apis/business-lodging.js +5 -0
  9. package/dist/src/apis/business-notifications.js +5 -0
  10. package/dist/src/apis/business-performance.js +5 -0
  11. package/dist/src/apis/business-place-actions.js +5 -0
  12. package/dist/src/apis/business-v4.js +52 -0
  13. package/dist/src/apis/business-verifications.js +5 -0
  14. package/dist/src/apis/cm360.js +4 -0
  15. package/dist/src/apis/crux.js +4 -0
  16. package/dist/src/apis/data-manager.js +4 -0
  17. package/dist/src/apis/dv360.js +4 -0
  18. package/dist/src/apis/ga4-admin-alpha.js +4 -0
  19. package/dist/src/apis/ga4-admin.js +1 -288
  20. package/dist/src/apis/ga4-data-alpha.js +4 -0
  21. package/dist/src/apis/ga4-data.js +1 -69
  22. package/dist/src/apis/gsc.js +5 -58
  23. package/dist/src/apis/gtm.js +1 -189
  24. package/dist/src/apis/indexing.js +37 -15
  25. package/dist/src/apis/merchant-accounts.js +4 -0
  26. package/dist/src/apis/merchant-conversions.js +4 -0
  27. package/dist/src/apis/merchant-datasources.js +4 -0
  28. package/dist/src/apis/merchant-inventories.js +4 -0
  29. package/dist/src/apis/merchant-issueresolution.js +4 -0
  30. package/dist/src/apis/merchant-lfp.js +4 -0
  31. package/dist/src/apis/merchant-loyaltycustomers.js +4 -0
  32. package/dist/src/apis/merchant-notifications.js +4 -0
  33. package/dist/src/apis/merchant-ordertracking.js +4 -0
  34. package/dist/src/apis/merchant-products.js +4 -0
  35. package/dist/src/apis/merchant-promotions.js +4 -0
  36. package/dist/src/apis/merchant-quota.js +4 -0
  37. package/dist/src/apis/merchant-reports.js +4 -0
  38. package/dist/src/apis/merchant-reviews.js +4 -0
  39. package/dist/src/apis/pagespeed.js +4 -0
  40. package/dist/src/apis/places.js +15 -0
  41. package/dist/src/apis/postmaster.js +4 -0
  42. package/dist/src/apis/sa360-legacy.js +4 -0
  43. package/dist/src/apis/sa360.js +12 -0
  44. package/dist/src/apis/sheets.js +4 -0
  45. package/dist/src/apis/site-verification.js +4 -0
  46. package/dist/src/apis/web-risk.js +14 -0
  47. package/dist/src/apis/youtube-analytics.js +4 -0
  48. package/dist/src/apis/youtube-reporting.js +4 -0
  49. package/dist/src/apis/youtube.js +4 -0
  50. package/dist/src/catalog.js +203 -0
  51. package/dist/src/credentials.js +316 -0
  52. package/dist/src/errors.js +16 -0
  53. package/dist/src/json.js +11 -0
  54. package/dist/src/plan.js +315 -0
  55. package/dist/src/render.js +183 -0
  56. package/dist/src/run.js +263 -0
  57. package/dist/src/transport/batch.js +75 -0
  58. package/dist/src/transport/download.js +15 -0
  59. package/dist/src/transport/upload.js +81 -0
  60. package/dist/src/transport.js +99 -0
  61. package/dist/src/types.js +1 -13
  62. package/package.json +22 -7
  63. package/dist/src/apis/ga4-admin-v1alpha.js +0 -280
  64. package/dist/src/auth.js +0 -172
  65. package/dist/src/config.js +0 -89
  66. package/dist/src/manifest.js +0 -69
  67. package/dist/src/rest.js +0 -90
@@ -1,18 +1,40 @@
1
- const I = ['indexing'];
1
+ // Indexing API v3: the discovery entry plus the `+publishBatch` helper (epic #30 D7, D10).
2
+ import { GapiError } from '../errors.js';
3
+ import { batchPlan, plan } from '../plan.js';
4
+ const PUBLISH = 'indexing.urlNotifications.publish';
5
+ const LINE = /^(URL_UPDATED|URL_DELETED)\s+(\S+)$/;
6
+ // One notification per line; blank and `#` lines are skipped. Each line becomes one batch
7
+ // part planned like `gapi indexing urlNotifications publish`, so every Planner rule applies.
8
+ const publishBatch = {
9
+ summary: '<file> publish URL_UPDATED|URL_DELETED <url> lines, 100 per batch call',
10
+ options: {},
11
+ plan: async ({ model, args, flags, settings, read }) => {
12
+ if (args.length !== 1)
13
+ throw new GapiError(3, 'usage: gapi indexing +publishBatch <file>');
14
+ const [file] = args;
15
+ const notes = [];
16
+ for (const [i, raw] of (await read(file)).split(/\r?\n/).entries()) {
17
+ const line = raw.trim();
18
+ if (line === '' || line.startsWith('#'))
19
+ continue;
20
+ const match = LINE.exec(line);
21
+ if (!match) {
22
+ throw new GapiError(3, `${file}:${i + 1}: expected URL_UPDATED <url> or URL_DELETED <url>`);
23
+ }
24
+ notes.push({ url: match[2], type: match[1] });
25
+ }
26
+ if (notes.length === 0)
27
+ throw new GapiError(3, `${file}: no notifications`);
28
+ const dryRun = flags['dry-run'] === true;
29
+ const parts = notes.map((n) => plan(model, PUBLISH, { body: n, dryRun }, settings));
30
+ return {
31
+ ...batchPlan(model, parts),
32
+ decode: (results) => Array.isArray(results) ? results.map((r, i) => ({ ...notes[i], ...r })) : results,
33
+ };
34
+ },
35
+ };
2
36
  export default {
3
37
  api: 'indexing',
4
- baseUrl: 'https://indexing.googleapis.com',
5
- resources: {
6
- 'url-notifications': {
7
- // POST body: { url, type: URL_UPDATED | URL_DELETED }.
8
- publish: { httpMethod: 'POST', pathTemplate: '/v3/urlNotifications:publish', scopes: I },
9
- // GET ?url= ; the url is a trailing positional (query seam), not a path param.
10
- 'get-metadata': {
11
- httpMethod: 'GET',
12
- pathTemplate: '/v3/urlNotifications/metadata',
13
- scopes: I,
14
- query: ['url'],
15
- },
16
- },
17
- },
38
+ discovery: 'https://indexing.googleapis.com/$discovery/rest?version=v3',
39
+ helpers: { '+publishBatch': publishBatch },
18
40
  };
@@ -0,0 +1,4 @@
1
+ export default {
2
+ api: 'merchant-accounts',
3
+ discovery: 'https://merchantapi.googleapis.com/$discovery/rest?version=accounts_v1',
4
+ };
@@ -0,0 +1,4 @@
1
+ export default {
2
+ api: 'merchant-conversions',
3
+ discovery: 'https://merchantapi.googleapis.com/$discovery/rest?version=conversions_v1',
4
+ };
@@ -0,0 +1,4 @@
1
+ export default {
2
+ api: 'merchant-datasources',
3
+ discovery: 'https://merchantapi.googleapis.com/$discovery/rest?version=datasources_v1',
4
+ };
@@ -0,0 +1,4 @@
1
+ export default {
2
+ api: 'merchant-inventories',
3
+ discovery: 'https://merchantapi.googleapis.com/$discovery/rest?version=inventories_v1',
4
+ };
@@ -0,0 +1,4 @@
1
+ export default {
2
+ api: 'merchant-issueresolution',
3
+ discovery: 'https://merchantapi.googleapis.com/$discovery/rest?version=issueresolution_v1',
4
+ };
@@ -0,0 +1,4 @@
1
+ export default {
2
+ api: 'merchant-lfp',
3
+ discovery: 'https://merchantapi.googleapis.com/$discovery/rest?version=lfp_v1',
4
+ };
@@ -0,0 +1,4 @@
1
+ export default {
2
+ api: 'merchant-loyaltycustomers',
3
+ discovery: 'https://merchantapi.googleapis.com/$discovery/rest?version=loyaltycustomers_v1',
4
+ };
@@ -0,0 +1,4 @@
1
+ export default {
2
+ api: 'merchant-notifications',
3
+ discovery: 'https://merchantapi.googleapis.com/$discovery/rest?version=notifications_v1',
4
+ };
@@ -0,0 +1,4 @@
1
+ export default {
2
+ api: 'merchant-ordertracking',
3
+ discovery: 'https://merchantapi.googleapis.com/$discovery/rest?version=ordertracking_v1',
4
+ };
@@ -0,0 +1,4 @@
1
+ export default {
2
+ api: 'merchant-products',
3
+ discovery: 'https://merchantapi.googleapis.com/$discovery/rest?version=products_v1',
4
+ };
@@ -0,0 +1,4 @@
1
+ export default {
2
+ api: 'merchant-promotions',
3
+ discovery: 'https://merchantapi.googleapis.com/$discovery/rest?version=promotions_v1',
4
+ };
@@ -0,0 +1,4 @@
1
+ export default {
2
+ api: 'merchant-quota',
3
+ discovery: 'https://merchantapi.googleapis.com/$discovery/rest?version=quota_v1',
4
+ };
@@ -0,0 +1,4 @@
1
+ export default {
2
+ api: 'merchant-reports',
3
+ discovery: 'https://merchantapi.googleapis.com/$discovery/rest?version=reports_v1',
4
+ };
@@ -0,0 +1,4 @@
1
+ export default {
2
+ api: 'merchant-reviews',
3
+ discovery: 'https://merchantapi.googleapis.com/$discovery/rest?version=reviews_v1alpha',
4
+ };
@@ -0,0 +1,4 @@
1
+ export default {
2
+ api: 'pagespeed',
3
+ discovery: 'https://pagespeedonline.googleapis.com/$discovery/rest?version=v5',
4
+ };
@@ -0,0 +1,15 @@
1
+ const billed = {
2
+ reason: 'Free monthly caps, then billed',
3
+ source: 'https://developers.google.com/maps/billing-and-pricing/pricing',
4
+ };
5
+ export default {
6
+ api: 'places',
7
+ discovery: 'https://places.googleapis.com/$discovery/rest?version=v1',
8
+ paid: {
9
+ 'places.places.autocomplete': billed,
10
+ 'places.places.get': billed,
11
+ 'places.places.photos.getMedia': billed,
12
+ 'places.places.searchNearby': billed,
13
+ 'places.places.searchText': billed,
14
+ },
15
+ };
@@ -0,0 +1,4 @@
1
+ export default {
2
+ api: 'postmaster',
3
+ discovery: 'https://gmailpostmastertools.googleapis.com/$discovery/rest?version=v2',
4
+ };
@@ -0,0 +1,4 @@
1
+ export default {
2
+ api: 'sa360-legacy',
3
+ discovery: 'https://doubleclicksearch.googleapis.com/$discovery/rest?version=v2',
4
+ };
@@ -0,0 +1,12 @@
1
+ export default {
2
+ api: 'sa360',
3
+ discovery: 'https://searchads360.googleapis.com/$discovery/rest?version=v0',
4
+ headers: [
5
+ {
6
+ name: 'login-customer-id',
7
+ flag: 'login-customer-id',
8
+ env: 'SA360_LOGIN_CUSTOMER_ID',
9
+ configKey: 'sa360-login-customer-id',
10
+ },
11
+ ],
12
+ };
@@ -0,0 +1,4 @@
1
+ export default {
2
+ api: 'sheets',
3
+ discovery: 'https://sheets.googleapis.com/$discovery/rest?version=v4',
4
+ };
@@ -0,0 +1,4 @@
1
+ export default {
2
+ api: 'site-verification',
3
+ discovery: 'https://siteverification.googleapis.com/$discovery/rest?version=v1',
4
+ };
@@ -0,0 +1,14 @@
1
+ export default {
2
+ api: 'web-risk',
3
+ discovery: 'https://webrisk.googleapis.com/$discovery/rest?version=v1',
4
+ paid: {
5
+ 'webrisk.hashes.search': {
6
+ reason: 'Paid only',
7
+ source: 'https://cloud.google.com/web-risk/pricing',
8
+ },
9
+ 'webrisk.projects.submissions.create': {
10
+ reason: 'Closed to new customers',
11
+ source: 'https://docs.cloud.google.com/web-risk/docs/submission-api',
12
+ },
13
+ },
14
+ };
@@ -0,0 +1,4 @@
1
+ export default {
2
+ api: 'youtube-analytics',
3
+ discovery: 'https://youtubeanalytics.googleapis.com/$discovery/rest?version=v2',
4
+ };
@@ -0,0 +1,4 @@
1
+ export default {
2
+ api: 'youtube-reporting',
3
+ discovery: 'https://youtubereporting.googleapis.com/$discovery/rest?version=v1',
4
+ };
@@ -0,0 +1,4 @@
1
+ export default {
2
+ api: 'youtube',
3
+ discovery: 'https://youtube.googleapis.com/$discovery/rest?version=v3',
4
+ };
@@ -0,0 +1,203 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ // Catalog: the API entries found by glob, their discovery documents behind a
10
+ // 24 h disk cache, and the one normalized ApiModel per API (epic #30 D2, D8, D13).
11
+ import { mkdir, readdir, readFile, rename, stat, writeFile } from 'node:fs/promises';
12
+ import { basename, dirname, extname, join } from 'node:path';
13
+ import { pathToFileURL } from 'node:url';
14
+ import { GapiError } from './errors.js';
15
+ import { isRecord, parseOrText } from './json.js';
16
+ const TTL_MS = 24 * 60 * 60 * 1000;
17
+ const AUTH_PREFIX = 'https://www.googleapis.com/auth/';
18
+ // Scopes gapi never requests (D8); `drive.*` is matched by prefix.
19
+ const NEVER = new Set(['drive', 'openid', 'email', 'profile', 'display-video-user-management']);
20
+ // Discovery reference: `kind` is "The fixed string discovery#restDescription."
21
+ const isRestDescription = (v) => isRecord(v) && v.kind === 'discovery#restDescription';
22
+ export async function openCatalog({ dir, fetch, warn, apisDir = join(import.meta.dirname, 'apis'), }) {
23
+ const entries = await loadEntries(apisDir);
24
+ const cacheDir = join(dir, 'cache');
25
+ const models = new Map();
26
+ // A non-2xx answer or a body that is not a discovery document is a failed fetch, never cached.
27
+ async function download(url) {
28
+ const res = await fetch(url);
29
+ if (!res.ok)
30
+ throw new Error(`HTTP ${res.status}`);
31
+ const text = await res.text();
32
+ const doc = JSON.parse(text);
33
+ if (!isRestDescription(doc))
34
+ throw new Error('response is not a discovery#restDescription');
35
+ return { text, doc };
36
+ }
37
+ // A cache file that is unreadable, not JSON or not a discovery document reads as absent.
38
+ const cached = async (file) => {
39
+ const doc = await readFile(file, 'utf8').then(parseOrText, () => undefined);
40
+ return isRestDescription(doc) ? doc : undefined;
41
+ };
42
+ async function document(entry, refresh) {
43
+ const file = join(cacheDir, `${entry.api}.json`);
44
+ const mtime = await stat(file).then((s) => s.mtimeMs, () => undefined);
45
+ if (!refresh && mtime !== undefined && Date.now() - mtime < TTL_MS) {
46
+ const doc = await cached(file);
47
+ if (doc)
48
+ return doc;
49
+ }
50
+ const got = await download(entry.discovery).catch((e) => e instanceof Error ? e : new Error(String(e)));
51
+ if (!(got instanceof Error)) {
52
+ await writeAtomic(file, got.text);
53
+ return got.doc;
54
+ }
55
+ const stale = mtime === undefined ? undefined : await cached(file);
56
+ if (!stale) {
57
+ throw new GapiError(4, `discovery fetch failed for ${entry.api}: ${got.message}; retry: gapi discovery refresh ${entry.api}`);
58
+ }
59
+ warn(`discovery fetch failed for ${entry.api}: ${got.message}; using cached copy`);
60
+ return stale;
61
+ }
62
+ async function load(api, { refresh = false } = {}) {
63
+ const memo = models.get(api);
64
+ if (memo && !refresh)
65
+ return memo;
66
+ const entry = Object.hasOwn(entries, api) ? entries[api] : undefined;
67
+ if (!entry)
68
+ throw new GapiError(3, `unknown api: ${api}`);
69
+ const model = toModel(await document(entry, refresh), entry);
70
+ models.set(api, model);
71
+ return model;
72
+ }
73
+ return { entries, load, cacheDir };
74
+ }
75
+ // `https://www.googleapis.com/auth/x` → `x`; any other scope URL stays as is.
76
+ export const shortScope = (scope) => scope.startsWith(AUTH_PREFIX) ? scope.slice(AUTH_PREFIX.length) : scope;
77
+ const isNever = (scope) => {
78
+ const short = shortScope(scope);
79
+ return NEVER.has(short) || short.startsWith('drive.');
80
+ };
81
+ function toModel(doc, entry) {
82
+ const scopes = (entry.scopes ?? []).map((s) => AUTH_PREFIX + s);
83
+ const byId = new Map();
84
+ const build = (node, words) => ({
85
+ methods: new Map(Object.entries(node.methods ?? {}).map(([key, m]) => {
86
+ const method = toMethod(m, [...words, key], scopes, doc.schemas ?? {});
87
+ byId.set(method.id, method);
88
+ return [key, method];
89
+ })),
90
+ resources: new Map(Object.entries(node.resources ?? {}).map(([key, r]) => [key, build(r, [...words, key])])),
91
+ });
92
+ const root = build(doc, []);
93
+ // Auth kind first (D8): no grantable scope anywhere and no entry scopes → API key.
94
+ const listed = [
95
+ ...Object.keys(doc.auth?.oauth2?.scopes ?? {}),
96
+ ...[...byId.values()].flatMap((m) => m.scopes),
97
+ ];
98
+ const auth = scopes.length === 0 && listed.every(isNever) ? 'apiKey' : 'oauth';
99
+ if (auth === 'oauth') {
100
+ for (const m of byId.values())
101
+ m.unavailable = m.scopes.length > 0 && m.scopes.every(isNever);
102
+ }
103
+ const problems = [];
104
+ const overlay = (kind, values, apply) => {
105
+ for (const [id, value] of Object.entries(values ?? {})) {
106
+ const m = byId.get(id);
107
+ if (m)
108
+ apply(m, value);
109
+ else
110
+ problems.push(`${kind} ${id}: not in the document`);
111
+ }
112
+ };
113
+ overlay('retired', entry.retired, (m, v) => {
114
+ m.retired = v;
115
+ });
116
+ overlay('deprecated', entry.deprecated, (m, v) => {
117
+ m.deprecated = v;
118
+ });
119
+ overlay('paid', entry.paid, (m, v) => {
120
+ m.paid = v;
121
+ });
122
+ return {
123
+ ...root,
124
+ api: entry.api,
125
+ name: doc.name ?? entry.api,
126
+ version: doc.version ?? '',
127
+ title: doc.title,
128
+ description: doc.description,
129
+ documentationLink: doc.documentationLink,
130
+ rootUrl: doc.rootUrl ?? '',
131
+ servicePath: doc.servicePath ?? '',
132
+ auth,
133
+ scopes,
134
+ byId,
135
+ parameters: new Map(Object.entries(doc.parameters ?? {})),
136
+ schemas: new Map(Object.entries(doc.schemas ?? {})),
137
+ headers: entry.headers,
138
+ problems,
139
+ };
140
+ }
141
+ // A method whose document lists no scopes takes the entry scopes (D8: `business-*`).
142
+ function toMethod(m, words, entryScopes, schemas) {
143
+ const scopes = m.scopes?.length ? m.scopes : entryScopes;
144
+ const parameters = new Map(Object.entries(m.parameters ?? {}));
145
+ const schemaHas = (ref, field) => {
146
+ const properties = ref && Object.hasOwn(schemas, ref.$ref) ? schemas[ref.$ref].properties : {};
147
+ return isRecord(properties) && Object.hasOwn(properties, field);
148
+ };
149
+ return {
150
+ id: m.id,
151
+ words,
152
+ httpMethod: m.httpMethod,
153
+ path: m.path,
154
+ description: m.description,
155
+ parameters,
156
+ parameterOrder: m.parameterOrder ?? [],
157
+ request: m.request?.$ref,
158
+ response: m.response?.$ref,
159
+ scopes,
160
+ scope: scopes.find((s) => !isNever(s)),
161
+ unavailable: false,
162
+ deprecated: m.deprecated ? {} : undefined,
163
+ upload: m.supportsMediaUpload
164
+ ? {
165
+ accept: m.mediaUpload?.accept ?? [],
166
+ maxSize: m.mediaUpload?.maxSize,
167
+ resumable: m.mediaUpload?.protocols?.resumable !== undefined,
168
+ }
169
+ : undefined,
170
+ download: m.supportsMediaDownload ? { service: m.useMediaDownloadService === true } : undefined,
171
+ // D6: a `pageToken` query parameter, else a request field answered by `nextPageToken`.
172
+ paging: parameters.get('pageToken')?.location === 'query'
173
+ ? 'query'
174
+ : schemaHas(m.request, 'pageToken') && schemaHas(m.response, 'nextPageToken')
175
+ ? 'body'
176
+ : undefined,
177
+ };
178
+ }
179
+ // The two fields every API entry sets; `satisfies ApiEntry` in the file checks the rest.
180
+ const isApiEntry = (v) => isRecord(v) && typeof v.api === 'string' && typeof v.discovery === 'string';
181
+ // Dev (.ts) and published (.js) entries, keyed by `api`; D1 ties `api` to the file name.
182
+ async function loadEntries(dir) {
183
+ const files = (await readdir(dir)).filter((f) => (f.endsWith('.ts') || f.endsWith('.js')) && !f.endsWith('.d.ts'));
184
+ const entries = {};
185
+ for (const f of files) {
186
+ const file = join(dir, f);
187
+ const e = (await import(__rewriteRelativeImportExtension(pathToFileURL(file).href))).default;
188
+ if (!isApiEntry(e))
189
+ throw new Error(`${file}: default export is not an API entry`);
190
+ const name = basename(f, extname(f));
191
+ if (e.api !== name)
192
+ throw new Error(`${file}: api '${e.api}' must equal '${name}'`);
193
+ entries[e.api] = e;
194
+ }
195
+ return entries;
196
+ }
197
+ // Temp file + rename so a reader never sees a half-written document.
198
+ async function writeAtomic(file, text) {
199
+ await mkdir(dirname(file), { recursive: true, mode: 0o700 });
200
+ const tmp = `${file}.${process.pid}.tmp`;
201
+ await writeFile(tmp, text, { mode: 0o600 });
202
+ await rename(tmp, file);
203
+ }