google-apis-cli 0.1.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.
- package/LICENSE +21 -0
- package/README.md +79 -0
- package/dist/bin/gapi.js +138 -0
- package/dist/src/apis/ads.js +54 -0
- package/dist/src/apis/ga4-admin-v1alpha.js +280 -0
- package/dist/src/apis/ga4-admin.js +291 -0
- package/dist/src/apis/ga4-data.js +72 -0
- package/dist/src/apis/gsc.js +63 -0
- package/dist/src/apis/gtm.js +192 -0
- package/dist/src/apis/indexing.js +18 -0
- package/dist/src/auth.js +172 -0
- package/dist/src/config.js +89 -0
- package/dist/src/manifest.js +69 -0
- package/dist/src/rest.js +90 -0
- package/dist/src/types.js +13 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 google-apis-cli contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# google-apis-cli (`gapi`)
|
|
2
|
+
|
|
3
|
+
`gapi` is one CLI for Google Ads, GTM, GA4 (Admin + Data), Search Console, and Indexing.
|
|
4
|
+
|
|
5
|
+
- One login. One refresh token. All scopes. Read and write.
|
|
6
|
+
- One runtime dependency: `google-auth-library` (Google). Nothing third-party.
|
|
7
|
+
- All network calls use native `fetch`.
|
|
8
|
+
- TypeScript 7 (strict), Node 24. Node runs `.ts` directly (type-stripping) — no build step in dev. Biome for lint + format. pnpm 12 (corepack-pinned) for package management.
|
|
9
|
+
- It replaces the 4 fragmented Google MCPs in vmCODE.
|
|
10
|
+
|
|
11
|
+
Command shape: `gapi <api> <resource> <verb> [--json|--raw]`
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
npm i -g google-apis-cli # installs the `gapi` bin
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Needs **Node ≥ 24** — `gapi` ships as TypeScript and runs on Node's native type-stripping, so there is no build step and no bundle.
|
|
20
|
+
|
|
21
|
+
## Start here
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
gapi auth setup # store your own OAuth Desktop client (id + secret)
|
|
25
|
+
gapi auth login # one consent, all 13 scopes
|
|
26
|
+
gapi auth status # account, refresh token, granted scopes
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
First run, or a console step fails? **[docs/SETUP.md](docs/SETUP.md)** walks the five steps and
|
|
30
|
+
the traps in each. `gapi` is multi-tenant: every operator brings their own Cloud project.
|
|
31
|
+
|
|
32
|
+
## OAuth App Verification
|
|
33
|
+
|
|
34
|
+
`gapi` asks for sensitive scopes (`analytics.manage.users`, `tagmanager.manage.*`, `adwords`, `webmasters`).
|
|
35
|
+
|
|
36
|
+
- **Testing mode** (default) works for all commands, but the refresh token expires every ~7 days — re-run `gapi auth login` when it does.
|
|
37
|
+
- **Production** removes that expiry, but Google requires **OAuth App Verification** first: a privacy-policy URL, a homepage, and a reviewed consent screen. Review timeline is not guaranteed.
|
|
38
|
+
|
|
39
|
+
Testing mode is enough to use the CLI; Production is the no-expiry target. Until verification passes, stay in Testing and re-auth on the ~7-day cadence.
|
|
40
|
+
|
|
41
|
+
## Flow
|
|
42
|
+
|
|
43
|
+
The project has 3 parts. A call moves through them in order.
|
|
44
|
+
|
|
45
|
+
| Part | File | Function |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| **auth** | `src/auth.ts` | `gapi auth login` gets one OAuth consent. It stores the refresh token in `~/.config/gapi/credentials.json` (mode 600). `getAccessToken()` refreshes the access token. Only part with real handlers. |
|
|
48
|
+
| **manifest** | `src/apis/*.ts` | Each API is data, not code: `resource → verb → { httpMethod, pathTemplate, scopes, requiredHeaders?, decoder?, listKey?, query? }`. One file per surface. A glob finds them. No central registry. Row/seam types live in `src/types.ts`. |
|
|
49
|
+
| **executor seam** | `src/rest.ts` | `execute(manifestRow, params, { tokenProvider, fetch })` builds the URL, adds headers, follows pagination, applies the `decoder`, and formats `--json`/`--raw`. All network calls pass here. It is the one test seam. |
|
|
50
|
+
|
|
51
|
+
`bin/gapi.ts` parses argv. The dispatch, `--help`, and URL resolution all come from the manifests.
|
|
52
|
+
|
|
53
|
+
## Where to look
|
|
54
|
+
|
|
55
|
+
| For | Read |
|
|
56
|
+
|---|---|
|
|
57
|
+
| Onboarding a Google account: consent, Ads access level, customer IDs, runtime errors | **[docs/SETUP.md](docs/SETUP.md)** |
|
|
58
|
+
| What the CLI does, decisions, scopes, endpoints, risks | **[docs/SPEC.md](docs/SPEC.md)** — behavior source of truth |
|
|
59
|
+
| How work is tracked (issues, blocking edges) | [docs/agents/issue-tracker.md](docs/agents/issue-tracker.md) |
|
|
60
|
+
| Triage labels | [docs/agents/triage-labels.md](docs/agents/triage-labels.md) |
|
|
61
|
+
| Domain vocabulary (CONTEXT.md, ADRs) | [docs/agents/domain.md](docs/agents/domain.md) |
|
|
62
|
+
| Real commands and scripts | `package.json`, `gapi --help` |
|
|
63
|
+
|
|
64
|
+
## Add an API or a resource
|
|
65
|
+
|
|
66
|
+
- New resource = one manifest row.
|
|
67
|
+
- New API = one new `src/apis/<api>.ts` file (default-export `satisfies Manifest`). The glob finds it. No shared file changes.
|
|
68
|
+
- Accept rule — **coverage-oracle**: the self-check fails if the manifest does not cover 100% of the discovery-doc resources.
|
|
69
|
+
- Test through the executor seam. Inject a fake `tokenProvider` and `fetch`. Use no real network.
|
|
70
|
+
- Before commit: `pnpm typecheck` (tsc --noEmit) · `pnpm lint` (biome) · `pnpm test`.
|
|
71
|
+
- Fix lint by fixing the code it flags — never `biome --unsafe`, never disable a rule to clear it.
|
|
72
|
+
|
|
73
|
+
## Agent skills
|
|
74
|
+
|
|
75
|
+
| Topic | Rule | Detail |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| Issue tracker | GitHub issues via `gh` | [docs/agents/issue-tracker.md](docs/agents/issue-tracker.md) |
|
|
78
|
+
| Triage labels | 5 canonical labels (string == role) | [docs/agents/triage-labels.md](docs/agents/triage-labels.md) |
|
|
79
|
+
| Domain docs | Single-context (`CONTEXT.md` + `docs/adr/`) | [docs/agents/domain.md](docs/agents/domain.md) |
|
package/dist/bin/gapi.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// argv -> dispatch. The command tree, --help, and URL resolution all derive
|
|
3
|
+
// from the auto-discovered manifests (single source). Two branches step outside
|
|
4
|
+
// generic dispatch: `auth` (real handlers) and the `ads gaql` sugar (story 31,
|
|
5
|
+
// query positional + --customer/--stream). Ads adds the optional
|
|
6
|
+
// login-customer-id HTTP header (resolveAdsHeaders) on `ads` calls only.
|
|
7
|
+
import { parseArgs } from 'node:util';
|
|
8
|
+
import { gaqlBody } from '../src/apis/ads.js';
|
|
9
|
+
import { configureAuth, loginLoopback, loginManual, logout, makeTokenProvider, status, } from '../src/auth.js';
|
|
10
|
+
import { QUOTA_PROJECT_ENV, resolveAdsHeaders, resolveSetting } from '../src/config.js';
|
|
11
|
+
import { checkSchema, findRow, loadManifests } from '../src/manifest.js';
|
|
12
|
+
import { execute, pathParams } from '../src/rest.js';
|
|
13
|
+
const GLOBAL_OPTIONS = {
|
|
14
|
+
json: { type: 'boolean' },
|
|
15
|
+
raw: { type: 'boolean' },
|
|
16
|
+
help: { type: 'boolean' },
|
|
17
|
+
limit: { type: 'string' },
|
|
18
|
+
body: { type: 'string' },
|
|
19
|
+
project: { type: 'string' },
|
|
20
|
+
// Ads (see `ads gaql` sugar + resolveAdsHeaders): login-customer-id header,
|
|
21
|
+
// target account, and search/searchStream toggle.
|
|
22
|
+
'login-customer-id': { type: 'string' },
|
|
23
|
+
customer: { type: 'string' },
|
|
24
|
+
stream: { type: 'boolean' },
|
|
25
|
+
};
|
|
26
|
+
function printTree(manifests) {
|
|
27
|
+
console.log('gapi <api> <resource> <verb> [--json|--raw]\n');
|
|
28
|
+
console.log('auth setup | login [--manual] | status | logout\n');
|
|
29
|
+
for (const [api, m] of Object.entries(manifests).sort()) {
|
|
30
|
+
console.log(api);
|
|
31
|
+
for (const [resource, verbs] of Object.entries(m.resources).sort()) {
|
|
32
|
+
console.log(` ${resource} ${Object.keys(verbs).sort().join(' | ')}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function runAuth(sub, values) {
|
|
37
|
+
if (sub === 'login') {
|
|
38
|
+
const tokens = values.manual ? await loginManual() : await loginLoopback();
|
|
39
|
+
console.error('gapi: authorized (' +
|
|
40
|
+
(tokens.refresh_token ? 'refresh token stored' : 'no refresh token') +
|
|
41
|
+
')');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (sub === 'setup') {
|
|
45
|
+
await configureAuth();
|
|
46
|
+
return console.error('gapi: saved to ~/.config/gapi/config.json');
|
|
47
|
+
}
|
|
48
|
+
if (sub === 'status')
|
|
49
|
+
return console.log(JSON.stringify(status(), null, 2));
|
|
50
|
+
if (sub === 'logout') {
|
|
51
|
+
logout();
|
|
52
|
+
return console.error('gapi: credentials removed');
|
|
53
|
+
}
|
|
54
|
+
throw new Error(`unknown auth command: ${sub}`);
|
|
55
|
+
}
|
|
56
|
+
async function main() {
|
|
57
|
+
const argv = process.argv.slice(2);
|
|
58
|
+
const manifests = await loadManifests();
|
|
59
|
+
checkSchema(manifests);
|
|
60
|
+
if (argv.length === 0 || (argv[0] === '--help' && argv.length === 1)) {
|
|
61
|
+
printTree(manifests);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const [api, ...rest] = argv;
|
|
65
|
+
if (api === 'auth') {
|
|
66
|
+
const { values, positionals } = parseArgs({
|
|
67
|
+
args: rest,
|
|
68
|
+
options: { manual: { type: 'boolean' }, help: { type: 'boolean' } },
|
|
69
|
+
allowPositionals: true,
|
|
70
|
+
});
|
|
71
|
+
return runAuth(positionals[0], values);
|
|
72
|
+
}
|
|
73
|
+
if (!manifests[api])
|
|
74
|
+
throw new Error(`unknown api: ${api} (try \`gapi --help\`)`);
|
|
75
|
+
const { values, positionals } = parseArgs({
|
|
76
|
+
args: rest,
|
|
77
|
+
options: GLOBAL_OPTIONS,
|
|
78
|
+
allowPositionals: true,
|
|
79
|
+
});
|
|
80
|
+
const [resource, verb] = positionals;
|
|
81
|
+
if (values.help || !resource || !verb) {
|
|
82
|
+
printTree({ [api]: manifests[api] });
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
// `ads gaql` is the one sugar beyond generic dispatch (story 31): the query is
|
|
86
|
+
// a positional, --stream picks searchStream, --customer fills the path param.
|
|
87
|
+
const isGaql = api === 'ads' && resource === 'gaql';
|
|
88
|
+
const row = isGaql
|
|
89
|
+
? findRow(manifests, api, 'gaql', values.stream ? 'searchStream' : 'search')
|
|
90
|
+
: findRow(manifests, api, resource, verb);
|
|
91
|
+
if (!row)
|
|
92
|
+
throw new Error(`unknown command: ${api} ${resource} ${verb}`);
|
|
93
|
+
const params = {};
|
|
94
|
+
if (isGaql) {
|
|
95
|
+
if (!values.customer)
|
|
96
|
+
throw new Error('ads gaql requires --customer <id>');
|
|
97
|
+
params.customer = values.customer;
|
|
98
|
+
params.body = gaqlBody(verb); // `verb` is the GAQL query string here
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
// params = positional path args after verb, then row.query params as further
|
|
102
|
+
// positionals, plus body/limit from flags.
|
|
103
|
+
const pp = pathParams(row.pathTemplate);
|
|
104
|
+
pp.forEach((name, i) => {
|
|
105
|
+
if (positionals[2 + i] != null)
|
|
106
|
+
params[name] = positionals[2 + i];
|
|
107
|
+
});
|
|
108
|
+
(row.query ?? []).forEach((name, i) => {
|
|
109
|
+
const v = positionals[2 + pp.length + i];
|
|
110
|
+
if (v != null)
|
|
111
|
+
params[name] = v;
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (values.limit != null)
|
|
115
|
+
params.limit = values.limit;
|
|
116
|
+
if (values.body != null)
|
|
117
|
+
params.body = JSON.parse(values.body);
|
|
118
|
+
const quotaProject = resolveSetting(values, {
|
|
119
|
+
flag: 'project',
|
|
120
|
+
env: QUOTA_PROJECT_ENV,
|
|
121
|
+
key: 'project',
|
|
122
|
+
});
|
|
123
|
+
// Only Ads calls carry the optional login-customer-id header, so other APIs
|
|
124
|
+
// never send an Ads-specific header.
|
|
125
|
+
const extraHeaders = api === 'ads' ? resolveAdsHeaders(values) : undefined;
|
|
126
|
+
const { json, raw } = await execute(row, params, {
|
|
127
|
+
tokenProvider: makeTokenProvider(),
|
|
128
|
+
fetch,
|
|
129
|
+
quotaProject,
|
|
130
|
+
extraHeaders,
|
|
131
|
+
});
|
|
132
|
+
console.log(JSON.stringify(values.raw ? raw : json, null, 2));
|
|
133
|
+
}
|
|
134
|
+
main().catch((e) => {
|
|
135
|
+
const detail = e.body ? `\n${JSON.stringify(e.body.error ?? e.body, null, 2)}` : '';
|
|
136
|
+
console.error(`gapi: ${e.message}${detail}`);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const ADS = ['adwords'];
|
|
2
|
+
const C = '/v25/customers/{customer}';
|
|
3
|
+
// Build the search-request body from a bare GAQL string (story 31). Trivial by
|
|
4
|
+
// design — kept a named pure function so bin/gapi.ts's `gapi ads gaql "<q>"`
|
|
5
|
+
// sugar has one tested seam.
|
|
6
|
+
export const gaqlBody = (query) => ({ query });
|
|
7
|
+
// searchStream → JSON array of {results, ...} chunks; concat into one list.
|
|
8
|
+
const streamConcat = (data) => Array.isArray(data) ? data.flatMap((c) => c?.results ?? []) : data;
|
|
9
|
+
// Every :mutate service is the same shape: POST customers/{}/{collection}:mutate
|
|
10
|
+
// with a {operations:[...]} body. Only the collection segment varies.
|
|
11
|
+
const mutate = (collection) => ({
|
|
12
|
+
mutate: {
|
|
13
|
+
httpMethod: 'POST',
|
|
14
|
+
pathTemplate: `${C}/${collection}:mutate`,
|
|
15
|
+
scopes: ADS,
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
export default {
|
|
19
|
+
api: 'ads',
|
|
20
|
+
baseUrl: 'https://googleads.googleapis.com',
|
|
21
|
+
resources: {
|
|
22
|
+
customers: {
|
|
23
|
+
// Empty body; returns { resourceNames: ["customers/123", ...] }. Ignores
|
|
24
|
+
// login-customer-id (direct-access accounts only).
|
|
25
|
+
'list-accessible': {
|
|
26
|
+
httpMethod: 'GET',
|
|
27
|
+
pathTemplate: '/v25/customers:listAccessibleCustomers',
|
|
28
|
+
scopes: ADS,
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
gaql: {
|
|
32
|
+
// Body-paged: {query, pageToken}. Raw {results, nextPageToken} out.
|
|
33
|
+
search: {
|
|
34
|
+
httpMethod: 'POST',
|
|
35
|
+
pathTemplate: `${C}/googleAds:search`,
|
|
36
|
+
scopes: ADS,
|
|
37
|
+
},
|
|
38
|
+
// Streams the whole result set in one response (array of chunks).
|
|
39
|
+
searchStream: {
|
|
40
|
+
httpMethod: 'POST',
|
|
41
|
+
pathTemplate: `${C}/googleAds:searchStream`,
|
|
42
|
+
scopes: ADS,
|
|
43
|
+
decoder: streamConcat,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
// Core write surface (:mutate). Body: {operations:[{create|update|remove}]}.
|
|
47
|
+
campaigns: mutate('campaigns'),
|
|
48
|
+
'campaign-budgets': mutate('campaignBudgets'),
|
|
49
|
+
'campaign-criteria': mutate('campaignCriteria'),
|
|
50
|
+
'ad-groups': mutate('adGroups'),
|
|
51
|
+
'ad-group-ads': mutate('adGroupAds'),
|
|
52
|
+
'ad-group-criteria': mutate('adGroupCriteria'),
|
|
53
|
+
},
|
|
54
|
+
};
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
const RO = ['analytics.readonly'];
|
|
2
|
+
const RW = ['analytics.edit'];
|
|
3
|
+
const USERS = ['analytics.manage.users'];
|
|
4
|
+
export default {
|
|
5
|
+
api: 'ga4-admin-alpha',
|
|
6
|
+
baseUrl: 'https://analyticsadmin.googleapis.com',
|
|
7
|
+
resources: {
|
|
8
|
+
// --- access bindings (users/permissions), gated by analytics.manage.users ---
|
|
9
|
+
'access-bindings': {
|
|
10
|
+
list: {
|
|
11
|
+
httpMethod: 'GET',
|
|
12
|
+
pathTemplate: '/v1alpha/properties/{property}/accessBindings',
|
|
13
|
+
scopes: USERS,
|
|
14
|
+
listKey: 'accessBindings',
|
|
15
|
+
},
|
|
16
|
+
get: {
|
|
17
|
+
httpMethod: 'GET',
|
|
18
|
+
pathTemplate: '/v1alpha/properties/{property}/accessBindings/{accessBinding}',
|
|
19
|
+
scopes: USERS,
|
|
20
|
+
},
|
|
21
|
+
// The names to fetch are a trailing positional → ?names= (query seam).
|
|
22
|
+
'batch-get': {
|
|
23
|
+
httpMethod: 'GET',
|
|
24
|
+
pathTemplate: '/v1alpha/properties/{property}/accessBindings:batchGet',
|
|
25
|
+
scopes: USERS,
|
|
26
|
+
query: ['names'],
|
|
27
|
+
},
|
|
28
|
+
create: {
|
|
29
|
+
httpMethod: 'POST',
|
|
30
|
+
pathTemplate: '/v1alpha/properties/{property}/accessBindings',
|
|
31
|
+
scopes: USERS,
|
|
32
|
+
},
|
|
33
|
+
patch: {
|
|
34
|
+
httpMethod: 'PATCH',
|
|
35
|
+
pathTemplate: '/v1alpha/properties/{property}/accessBindings/{accessBinding}',
|
|
36
|
+
scopes: USERS,
|
|
37
|
+
},
|
|
38
|
+
delete: {
|
|
39
|
+
httpMethod: 'DELETE',
|
|
40
|
+
pathTemplate: '/v1alpha/properties/{property}/accessBindings/{accessBinding}',
|
|
41
|
+
scopes: USERS,
|
|
42
|
+
},
|
|
43
|
+
'batch-create': {
|
|
44
|
+
httpMethod: 'POST',
|
|
45
|
+
pathTemplate: '/v1alpha/properties/{property}/accessBindings:batchCreate',
|
|
46
|
+
scopes: USERS,
|
|
47
|
+
},
|
|
48
|
+
'batch-update': {
|
|
49
|
+
httpMethod: 'POST',
|
|
50
|
+
pathTemplate: '/v1alpha/properties/{property}/accessBindings:batchUpdate',
|
|
51
|
+
scopes: USERS,
|
|
52
|
+
},
|
|
53
|
+
'batch-delete': {
|
|
54
|
+
httpMethod: 'POST',
|
|
55
|
+
pathTemplate: '/v1alpha/properties/{property}/accessBindings:batchDelete',
|
|
56
|
+
scopes: USERS,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
'account-access-bindings': {
|
|
60
|
+
list: {
|
|
61
|
+
httpMethod: 'GET',
|
|
62
|
+
pathTemplate: '/v1alpha/accounts/{account}/accessBindings',
|
|
63
|
+
scopes: USERS,
|
|
64
|
+
listKey: 'accessBindings',
|
|
65
|
+
},
|
|
66
|
+
get: {
|
|
67
|
+
httpMethod: 'GET',
|
|
68
|
+
pathTemplate: '/v1alpha/accounts/{account}/accessBindings/{accessBinding}',
|
|
69
|
+
scopes: USERS,
|
|
70
|
+
},
|
|
71
|
+
// The names to fetch are a trailing positional → ?names= (query seam).
|
|
72
|
+
'batch-get': {
|
|
73
|
+
httpMethod: 'GET',
|
|
74
|
+
pathTemplate: '/v1alpha/accounts/{account}/accessBindings:batchGet',
|
|
75
|
+
scopes: USERS,
|
|
76
|
+
query: ['names'],
|
|
77
|
+
},
|
|
78
|
+
create: {
|
|
79
|
+
httpMethod: 'POST',
|
|
80
|
+
pathTemplate: '/v1alpha/accounts/{account}/accessBindings',
|
|
81
|
+
scopes: USERS,
|
|
82
|
+
},
|
|
83
|
+
patch: {
|
|
84
|
+
httpMethod: 'PATCH',
|
|
85
|
+
pathTemplate: '/v1alpha/accounts/{account}/accessBindings/{accessBinding}',
|
|
86
|
+
scopes: USERS,
|
|
87
|
+
},
|
|
88
|
+
delete: {
|
|
89
|
+
httpMethod: 'DELETE',
|
|
90
|
+
pathTemplate: '/v1alpha/accounts/{account}/accessBindings/{accessBinding}',
|
|
91
|
+
scopes: USERS,
|
|
92
|
+
},
|
|
93
|
+
'batch-create': {
|
|
94
|
+
httpMethod: 'POST',
|
|
95
|
+
pathTemplate: '/v1alpha/accounts/{account}/accessBindings:batchCreate',
|
|
96
|
+
scopes: USERS,
|
|
97
|
+
},
|
|
98
|
+
'batch-update': {
|
|
99
|
+
httpMethod: 'POST',
|
|
100
|
+
pathTemplate: '/v1alpha/accounts/{account}/accessBindings:batchUpdate',
|
|
101
|
+
scopes: USERS,
|
|
102
|
+
},
|
|
103
|
+
'batch-delete': {
|
|
104
|
+
httpMethod: 'POST',
|
|
105
|
+
pathTemplate: '/v1alpha/accounts/{account}/accessBindings:batchDelete',
|
|
106
|
+
scopes: USERS,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
// --- audiences (no delete: archived via :archive) ---
|
|
110
|
+
audiences: {
|
|
111
|
+
list: {
|
|
112
|
+
httpMethod: 'GET',
|
|
113
|
+
pathTemplate: '/v1alpha/properties/{property}/audiences',
|
|
114
|
+
scopes: RO,
|
|
115
|
+
listKey: 'audiences',
|
|
116
|
+
},
|
|
117
|
+
get: {
|
|
118
|
+
httpMethod: 'GET',
|
|
119
|
+
pathTemplate: '/v1alpha/properties/{property}/audiences/{audience}',
|
|
120
|
+
scopes: RO,
|
|
121
|
+
},
|
|
122
|
+
create: {
|
|
123
|
+
httpMethod: 'POST',
|
|
124
|
+
pathTemplate: '/v1alpha/properties/{property}/audiences',
|
|
125
|
+
scopes: RW,
|
|
126
|
+
},
|
|
127
|
+
patch: {
|
|
128
|
+
httpMethod: 'PATCH',
|
|
129
|
+
pathTemplate: '/v1alpha/properties/{property}/audiences/{audience}',
|
|
130
|
+
scopes: RW,
|
|
131
|
+
},
|
|
132
|
+
archive: {
|
|
133
|
+
httpMethod: 'POST',
|
|
134
|
+
pathTemplate: '/v1alpha/properties/{property}/audiences/{audience}:archive',
|
|
135
|
+
scopes: RW,
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
'calculated-metrics': {
|
|
139
|
+
list: {
|
|
140
|
+
httpMethod: 'GET',
|
|
141
|
+
pathTemplate: '/v1alpha/properties/{property}/calculatedMetrics',
|
|
142
|
+
scopes: RO,
|
|
143
|
+
listKey: 'calculatedMetrics',
|
|
144
|
+
},
|
|
145
|
+
get: {
|
|
146
|
+
httpMethod: 'GET',
|
|
147
|
+
pathTemplate: '/v1alpha/properties/{property}/calculatedMetrics/{calculatedMetric}',
|
|
148
|
+
scopes: RO,
|
|
149
|
+
},
|
|
150
|
+
create: {
|
|
151
|
+
httpMethod: 'POST',
|
|
152
|
+
pathTemplate: '/v1alpha/properties/{property}/calculatedMetrics',
|
|
153
|
+
scopes: RW,
|
|
154
|
+
},
|
|
155
|
+
patch: {
|
|
156
|
+
httpMethod: 'PATCH',
|
|
157
|
+
pathTemplate: '/v1alpha/properties/{property}/calculatedMetrics/{calculatedMetric}',
|
|
158
|
+
scopes: RW,
|
|
159
|
+
},
|
|
160
|
+
delete: {
|
|
161
|
+
httpMethod: 'DELETE',
|
|
162
|
+
pathTemplate: '/v1alpha/properties/{property}/calculatedMetrics/{calculatedMetric}',
|
|
163
|
+
scopes: RW,
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
'channel-groups': {
|
|
167
|
+
list: {
|
|
168
|
+
httpMethod: 'GET',
|
|
169
|
+
pathTemplate: '/v1alpha/properties/{property}/channelGroups',
|
|
170
|
+
scopes: RO,
|
|
171
|
+
listKey: 'channelGroups',
|
|
172
|
+
},
|
|
173
|
+
get: {
|
|
174
|
+
httpMethod: 'GET',
|
|
175
|
+
pathTemplate: '/v1alpha/properties/{property}/channelGroups/{channelGroup}',
|
|
176
|
+
scopes: RO,
|
|
177
|
+
},
|
|
178
|
+
create: {
|
|
179
|
+
httpMethod: 'POST',
|
|
180
|
+
pathTemplate: '/v1alpha/properties/{property}/channelGroups',
|
|
181
|
+
scopes: RW,
|
|
182
|
+
},
|
|
183
|
+
patch: {
|
|
184
|
+
httpMethod: 'PATCH',
|
|
185
|
+
pathTemplate: '/v1alpha/properties/{property}/channelGroups/{channelGroup}',
|
|
186
|
+
scopes: RW,
|
|
187
|
+
},
|
|
188
|
+
delete: {
|
|
189
|
+
httpMethod: 'DELETE',
|
|
190
|
+
pathTemplate: '/v1alpha/properties/{property}/channelGroups/{channelGroup}',
|
|
191
|
+
scopes: RW,
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
// --- subproperties & rollups ---
|
|
195
|
+
'rollup-properties': {
|
|
196
|
+
create: {
|
|
197
|
+
httpMethod: 'POST',
|
|
198
|
+
pathTemplate: '/v1alpha/properties:createRollupProperty',
|
|
199
|
+
scopes: RW,
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
subproperties: {
|
|
203
|
+
provision: {
|
|
204
|
+
httpMethod: 'POST',
|
|
205
|
+
pathTemplate: '/v1alpha/properties:provisionSubproperty',
|
|
206
|
+
scopes: RW,
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
'rollup-property-source-links': {
|
|
210
|
+
list: {
|
|
211
|
+
httpMethod: 'GET',
|
|
212
|
+
pathTemplate: '/v1alpha/properties/{property}/rollupPropertySourceLinks',
|
|
213
|
+
scopes: RO,
|
|
214
|
+
listKey: 'rollupPropertySourceLinks',
|
|
215
|
+
},
|
|
216
|
+
get: {
|
|
217
|
+
httpMethod: 'GET',
|
|
218
|
+
pathTemplate: '/v1alpha/properties/{property}/rollupPropertySourceLinks/{rollupPropertySourceLink}',
|
|
219
|
+
scopes: RO,
|
|
220
|
+
},
|
|
221
|
+
create: {
|
|
222
|
+
httpMethod: 'POST',
|
|
223
|
+
pathTemplate: '/v1alpha/properties/{property}/rollupPropertySourceLinks',
|
|
224
|
+
scopes: RW,
|
|
225
|
+
},
|
|
226
|
+
delete: {
|
|
227
|
+
httpMethod: 'DELETE',
|
|
228
|
+
pathTemplate: '/v1alpha/properties/{property}/rollupPropertySourceLinks/{rollupPropertySourceLink}',
|
|
229
|
+
scopes: RW,
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
'subproperty-event-filters': {
|
|
233
|
+
list: {
|
|
234
|
+
httpMethod: 'GET',
|
|
235
|
+
pathTemplate: '/v1alpha/properties/{property}/subpropertyEventFilters',
|
|
236
|
+
scopes: RO,
|
|
237
|
+
listKey: 'subpropertyEventFilters',
|
|
238
|
+
},
|
|
239
|
+
get: {
|
|
240
|
+
httpMethod: 'GET',
|
|
241
|
+
pathTemplate: '/v1alpha/properties/{property}/subpropertyEventFilters/{subpropertyEventFilter}',
|
|
242
|
+
scopes: RO,
|
|
243
|
+
},
|
|
244
|
+
create: {
|
|
245
|
+
httpMethod: 'POST',
|
|
246
|
+
pathTemplate: '/v1alpha/properties/{property}/subpropertyEventFilters',
|
|
247
|
+
scopes: RW,
|
|
248
|
+
},
|
|
249
|
+
patch: {
|
|
250
|
+
httpMethod: 'PATCH',
|
|
251
|
+
pathTemplate: '/v1alpha/properties/{property}/subpropertyEventFilters/{subpropertyEventFilter}',
|
|
252
|
+
scopes: RW,
|
|
253
|
+
},
|
|
254
|
+
delete: {
|
|
255
|
+
httpMethod: 'DELETE',
|
|
256
|
+
pathTemplate: '/v1alpha/properties/{property}/subpropertyEventFilters/{subpropertyEventFilter}',
|
|
257
|
+
scopes: RW,
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
// subproperty sync configs are auto-created → list/get/patch only.
|
|
261
|
+
'subproperty-sync-configs': {
|
|
262
|
+
list: {
|
|
263
|
+
httpMethod: 'GET',
|
|
264
|
+
pathTemplate: '/v1alpha/properties/{property}/subpropertySyncConfigs',
|
|
265
|
+
scopes: RO,
|
|
266
|
+
listKey: 'subpropertySyncConfigs',
|
|
267
|
+
},
|
|
268
|
+
get: {
|
|
269
|
+
httpMethod: 'GET',
|
|
270
|
+
pathTemplate: '/v1alpha/properties/{property}/subpropertySyncConfigs/{subpropertySyncConfig}',
|
|
271
|
+
scopes: RO,
|
|
272
|
+
},
|
|
273
|
+
patch: {
|
|
274
|
+
httpMethod: 'PATCH',
|
|
275
|
+
pathTemplate: '/v1alpha/properties/{property}/subpropertySyncConfigs/{subpropertySyncConfig}',
|
|
276
|
+
scopes: RW,
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
};
|