@malloydata/db-bigquery 0.0.430 → 0.0.432
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/dist/bigquery_connection.d.ts +19 -2
- package/dist/bigquery_connection.js +130 -2
- package/dist/index.js +38 -2
- package/package.json +2 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { Job, PagedResponse, Query, QueryResultsOptions, RowMetadata } from '@google-cloud/bigquery';
|
|
1
|
+
import type { BigQueryOptions, Job, PagedResponse, Query, QueryResultsOptions, RowMetadata } from '@google-cloud/bigquery';
|
|
2
2
|
import type bigquery from '@google-cloud/bigquery/build/src/types';
|
|
3
3
|
import type { ResourceStream } from '@google-cloud/paginator';
|
|
4
|
-
import type { Connection, ConnectionConfig, ConnectionParameterValue, MalloyQueryData, PersistSQLResults, QueryData, QueryRecord, QueryOptionsReader, QueryRunStats, RunSQLOptions, StreamingConnection, TableSourceDef, SQLSourceDef, SQLSourceRequest } from '@malloydata/malloy';
|
|
4
|
+
import type { Connection, ConnectionConfig, ConnectionConfigEntry, ConnectionParameterValue, MalloyQueryData, PersistSQLResults, QueryData, QueryRecord, QueryOptionsReader, QueryRunStats, RunSQLOptions, StreamingConnection, TableSourceDef, SQLSourceDef, SQLSourceRequest } from '@malloydata/malloy';
|
|
5
5
|
import type { TableMetadata } from '@malloydata/malloy/connection';
|
|
6
6
|
import { BaseConnection } from '@malloydata/malloy/connection';
|
|
7
7
|
export interface BigQueryManagerOptions {
|
|
@@ -17,6 +17,12 @@ interface CredentialBody {
|
|
|
17
17
|
client_email?: string;
|
|
18
18
|
private_key?: string;
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* A caller-supplied credential. Anything Malloy cannot build from config text
|
|
22
|
+
* — impersonation, workload identity federation, a proxied or test credential
|
|
23
|
+
* — arrives as one of these, from a host overlay.
|
|
24
|
+
*/
|
|
25
|
+
type AuthClient = BigQueryOptions['authClient'];
|
|
20
26
|
interface BigQueryConnectionConfiguration {
|
|
21
27
|
/** This ID is used for Bigquery Table Normalization */
|
|
22
28
|
projectId?: string;
|
|
@@ -28,6 +34,7 @@ interface BigQueryConnectionConfiguration {
|
|
|
28
34
|
credentials?: CredentialBody | {
|
|
29
35
|
[key: string]: ConnectionParameterValue;
|
|
30
36
|
};
|
|
37
|
+
authClient?: AuthClient;
|
|
31
38
|
setupSQL?: string;
|
|
32
39
|
}
|
|
33
40
|
interface BigQueryConnectionOptions extends ConnectionConfig {
|
|
@@ -37,6 +44,15 @@ interface BigQueryConnectionOptions extends ConnectionConfig {
|
|
|
37
44
|
serviceAccountKey?: {
|
|
38
45
|
[key: string]: ConnectionParameterValue;
|
|
39
46
|
};
|
|
47
|
+
/** The key file's contents, as a JSON string or base64-encoded JSON. */
|
|
48
|
+
serviceAccountKeyJson?: string;
|
|
49
|
+
authClient?: AuthClient;
|
|
50
|
+
/**
|
|
51
|
+
* The connection's entry as written, before overlay resolution. Set by the
|
|
52
|
+
* registered factory; needed only so `getDigest` can identify which auth
|
|
53
|
+
* client this connection got. See `authIdentityOf`.
|
|
54
|
+
*/
|
|
55
|
+
rawConfigData?: ConnectionConfigEntry;
|
|
40
56
|
location?: string;
|
|
41
57
|
maximumBytesBilled?: string;
|
|
42
58
|
timeoutMs?: string;
|
|
@@ -91,6 +107,7 @@ export declare class BigQueryConnection extends BaseConnection implements Connec
|
|
|
91
107
|
private config;
|
|
92
108
|
private location?;
|
|
93
109
|
private setupSQL;
|
|
110
|
+
private authIdentity;
|
|
94
111
|
constructor(option: BigQueryConnectionOptions, queryOptions?: QueryOptionsReader);
|
|
95
112
|
constructor(name: string, queryOptions?: QueryOptionsReader, config?: BigQueryConnectionConfiguration);
|
|
96
113
|
get dialectName(): string;
|
|
@@ -44,6 +44,115 @@ const googleCommon = __importStar(require("@google-cloud/common"));
|
|
|
44
44
|
const gaxios_1 = require("gaxios");
|
|
45
45
|
const malloy_1 = require("@malloydata/malloy");
|
|
46
46
|
const connection_1 = require("@malloydata/malloy/connection");
|
|
47
|
+
/**
|
|
48
|
+
* What to hash for the auth client, given the connection's unresolved config.
|
|
49
|
+
*
|
|
50
|
+
* An `AuthClient` is a live object with nothing stable to hash, so the digest
|
|
51
|
+
* uses the reference the config named it by — `{tenantAuth: "acme"}`. Without
|
|
52
|
+
* it, two connections against the same project holding different impersonated
|
|
53
|
+
* identities produce the same digest, and so the same BuildIDs, and one
|
|
54
|
+
* tenant is served rows persisted for another.
|
|
55
|
+
*
|
|
56
|
+
* The digest is therefore only as distinguishing as the reference path: an
|
|
57
|
+
* overlay whose path doesn't vary by identity leaves them indistinguishable
|
|
58
|
+
* here too.
|
|
59
|
+
*/
|
|
60
|
+
function authIdentityOf(rawConfigData) {
|
|
61
|
+
const reference = rawConfigData === null || rawConfigData === void 0 ? void 0 : rawConfigData['authClient'];
|
|
62
|
+
return reference === undefined ? undefined : JSON.stringify(reference);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* An `authClient` and a service account key are two answers to one question,
|
|
66
|
+
* and the SDK does not treat them as competing: `GoogleAuth` caches the
|
|
67
|
+
* `authClient` and never consults `credentials` or `keyFilename` again
|
|
68
|
+
* (`google-auth-library`, `googleauth.js`: `cachedCredential = opts.authClient`).
|
|
69
|
+
* A config carrying both therefore runs entirely on the auth client while the
|
|
70
|
+
* key sits there looking live, and whoever wrote it believes the wrong
|
|
71
|
+
* identity is executing their queries. Refuse it instead.
|
|
72
|
+
*/
|
|
73
|
+
function rejectCompetingCredentials(name, config) {
|
|
74
|
+
if (config.authClient === undefined)
|
|
75
|
+
return;
|
|
76
|
+
const alsoSet = [
|
|
77
|
+
config.credentials !== undefined ? 'a service account key' : undefined,
|
|
78
|
+
config.serviceAccountKeyPath !== undefined
|
|
79
|
+
? 'serviceAccountKeyPath'
|
|
80
|
+
: undefined,
|
|
81
|
+
].filter(what => what !== undefined);
|
|
82
|
+
if (alsoSet.length === 0)
|
|
83
|
+
return;
|
|
84
|
+
throw new Error(`Connection "${name}" sets authClient and also ${alsoSet.join(' and ')}. ` +
|
|
85
|
+
'An authClient replaces the credential entirely — the key would be ' +
|
|
86
|
+
'ignored — so supply one or the other.');
|
|
87
|
+
}
|
|
88
|
+
function isJsonObject(value) {
|
|
89
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The two credential shapes the SDK's `credentials` option accepts: a service
|
|
93
|
+
* account key, and an external account (workload identity federation) config,
|
|
94
|
+
* which carries no key of its own. Anything else — `{}`, a config file, half a
|
|
95
|
+
* key — is rejected here rather than at the first query, where it arrives as
|
|
96
|
+
* "the incoming JSON object does not contain a client_email field" and names
|
|
97
|
+
* nothing that would lead back to this property.
|
|
98
|
+
*/
|
|
99
|
+
function isCredentialObject(value) {
|
|
100
|
+
return ((typeof value['client_email'] === 'string' &&
|
|
101
|
+
typeof value['private_key'] === 'string') ||
|
|
102
|
+
value['type'] === 'external_account');
|
|
103
|
+
}
|
|
104
|
+
function isProbablyBase64(text) {
|
|
105
|
+
// `{` is not in the base64 alphabet, so JSON can never be mistaken for an
|
|
106
|
+
// encoding of itself. Everything else is treated as base64 and allowed to
|
|
107
|
+
// fail the parse below, which keeps the check to the one thing it can be
|
|
108
|
+
// certain about.
|
|
109
|
+
return !text.startsWith('{');
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* A service account key that arrives as a *string* rather than as structured
|
|
113
|
+
* config, in either of the two encodings a deployment can produce.
|
|
114
|
+
*
|
|
115
|
+
* `serviceAccountKey` is a `json`-typed property, and a json-typed slot takes
|
|
116
|
+
* its value literally: an `{env: "..."}` reference is never resolved in one,
|
|
117
|
+
* because reference indirection into structured config is exactly what the
|
|
118
|
+
* config compiler refuses. A deployment whose credentials live in an
|
|
119
|
+
* environment variable — the normal shape for a server — therefore cannot
|
|
120
|
+
* reach that slot at all, and the literal `{env: "..."}` object it does reach
|
|
121
|
+
* the SDK with fails as "the incoming JSON object does not contain a
|
|
122
|
+
* client_email field". This is a string slot, so a reference resolves and the
|
|
123
|
+
* value arrives here to be parsed.
|
|
124
|
+
*
|
|
125
|
+
* Base64 is accepted because a JSON object full of quotes and braces survives
|
|
126
|
+
* a shell, a CI secret editor, and a `.env` file poorly; base64 is one token
|
|
127
|
+
* that survives all three. Which one arrived is detected rather than declared,
|
|
128
|
+
* so a deployment that switches encodings doesn't also have to edit config.
|
|
129
|
+
*
|
|
130
|
+
* Nothing from `text` reaches the error message. It is a private key, and
|
|
131
|
+
* JSON.parse's own SyntaxError quotes the input it choked on.
|
|
132
|
+
*/
|
|
133
|
+
function credentialsFromJson(text) {
|
|
134
|
+
// Buffer's base64 decoder drops characters outside the alphabet rather than
|
|
135
|
+
// rejecting them, so a mangled value decodes to garbage instead of throwing.
|
|
136
|
+
// The parse below is what catches that, which is why one message has to
|
|
137
|
+
// cover both encodings: at this point either could have been intended.
|
|
138
|
+
const json = isProbablyBase64(text)
|
|
139
|
+
? Buffer.from(text, 'base64').toString('utf8')
|
|
140
|
+
: text;
|
|
141
|
+
let parsed;
|
|
142
|
+
try {
|
|
143
|
+
parsed = JSON.parse(json);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
throw new Error('serviceAccountKeyJson is neither JSON nor base64-encoded JSON. It ' +
|
|
147
|
+
'must hold the entire service account key file.');
|
|
148
|
+
}
|
|
149
|
+
if (!isJsonObject(parsed) || !isCredentialObject(parsed)) {
|
|
150
|
+
throw new Error('serviceAccountKeyJson parsed but is not a service account key: it ' +
|
|
151
|
+
'has no client_email and private_key. It must hold the entire key ' +
|
|
152
|
+
'file, not a fragment of one.');
|
|
153
|
+
}
|
|
154
|
+
return parsed;
|
|
155
|
+
}
|
|
47
156
|
// BigQuery label grammar: keys and values are lowercase, <=63 chars, and
|
|
48
157
|
// [a-z0-9_-]; keys must start with a lowercase letter. Values are transformed
|
|
49
158
|
// to fit (lowercase, disallowed chars -> '_', truncate); a key that can't be
|
|
@@ -294,12 +403,25 @@ class BigQueryConnection extends connection_1.BaseConnection {
|
|
|
294
403
|
this.name = arg;
|
|
295
404
|
}
|
|
296
405
|
else {
|
|
297
|
-
|
|
406
|
+
// Every key-bearing property is destructured out of `args`, so a key
|
|
407
|
+
// never lands in `this.config` — only the credentials object the SDK
|
|
408
|
+
// needs does. `rawConfigData` comes out for the same reason: it is
|
|
409
|
+
// read once, for the digest, and is not connection config.
|
|
410
|
+
const { name, client_email, private_key, serviceAccountKey, serviceAccountKeyJson, rawConfigData, ...args } = arg;
|
|
298
411
|
this.name = name;
|
|
412
|
+
this.authIdentity = authIdentityOf(rawConfigData);
|
|
299
413
|
config = args;
|
|
414
|
+
// Trimmed before it is looked at: a value that came through a here-doc,
|
|
415
|
+
// a `$(cat key.json)`, or a secret-store copy tends to carry a trailing
|
|
416
|
+
// newline, and both the encoding sniff and the emptiness check below
|
|
417
|
+
// would otherwise read it as content.
|
|
418
|
+
const keyJson = serviceAccountKeyJson === null || serviceAccountKeyJson === void 0 ? void 0 : serviceAccountKeyJson.trim();
|
|
300
419
|
if (serviceAccountKey) {
|
|
301
420
|
config.credentials = serviceAccountKey;
|
|
302
421
|
}
|
|
422
|
+
else if (keyJson) {
|
|
423
|
+
config.credentials = credentialsFromJson(keyJson);
|
|
424
|
+
}
|
|
303
425
|
else if (client_email || private_key) {
|
|
304
426
|
config.credentials = {
|
|
305
427
|
client_email,
|
|
@@ -307,10 +429,12 @@ class BigQueryConnection extends connection_1.BaseConnection {
|
|
|
307
429
|
};
|
|
308
430
|
}
|
|
309
431
|
}
|
|
432
|
+
rejectCompetingCredentials(this.name, config);
|
|
310
433
|
this.bigQuery = new bigquery_1.BigQuery({
|
|
311
434
|
userAgent: `Malloy/${malloy_1.Malloy.version}`,
|
|
312
435
|
keyFilename: config.serviceAccountKeyPath,
|
|
313
436
|
credentials: config.credentials,
|
|
437
|
+
authClient: config.authClient,
|
|
314
438
|
projectId: config.billingProjectId,
|
|
315
439
|
});
|
|
316
440
|
// record project ID because for unclear reasons we have to modify the project ID on the SDK when
|
|
@@ -354,7 +478,11 @@ class BigQueryConnection extends connection_1.BaseConnection {
|
|
|
354
478
|
return true;
|
|
355
479
|
}
|
|
356
480
|
getDigest() {
|
|
357
|
-
|
|
481
|
+
// Appended only when an auth client is in play, so connections that don't
|
|
482
|
+
// use one keep the digests — and so the persisted tables — they have now.
|
|
483
|
+
return this.authIdentity === undefined
|
|
484
|
+
? (0, malloy_1.makeDigest)('bigquery', this.projectId, this.setupSQL)
|
|
485
|
+
: (0, malloy_1.makeDigest)('bigquery', this.projectId, this.setupSQL, this.authIdentity);
|
|
358
486
|
}
|
|
359
487
|
get supportsNesting() {
|
|
360
488
|
return true;
|
package/dist/index.js
CHANGED
|
@@ -11,8 +11,11 @@ const malloy_1 = require("@malloydata/malloy");
|
|
|
11
11
|
const bigquery_connection_2 = require("./bigquery_connection");
|
|
12
12
|
(0, malloy_1.registerConnectionType)('bigquery', {
|
|
13
13
|
displayName: 'BigQuery',
|
|
14
|
-
factory: async (config) => {
|
|
15
|
-
|
|
14
|
+
factory: async (config, rawConfigData) => {
|
|
15
|
+
// rawConfigData rides in the options bag so `getDigest` can tell which
|
|
16
|
+
// auth client this connection was given; the constructor keeps it out of
|
|
17
|
+
// the retained config.
|
|
18
|
+
return new bigquery_connection_2.BigQueryConnection({ ...config, rawConfigData });
|
|
16
19
|
},
|
|
17
20
|
properties: [
|
|
18
21
|
{
|
|
@@ -34,6 +37,39 @@ const bigquery_connection_2 = require("./bigquery_connection");
|
|
|
34
37
|
type: 'json',
|
|
35
38
|
optional: true,
|
|
36
39
|
},
|
|
40
|
+
// The string-typed twin of `serviceAccountKey`. A `json` slot holds its
|
|
41
|
+
// value literally — an `{env: "..."}` reference is never resolved in one —
|
|
42
|
+
// so a deployment holding its key in an environment variable has no way to
|
|
43
|
+
// use the slot above. This is a `secret` string, where references do
|
|
44
|
+
// resolve, and the connection parses what arrives.
|
|
45
|
+
{
|
|
46
|
+
name: 'serviceAccountKeyJson',
|
|
47
|
+
displayName: 'Service Account Key (JSON)',
|
|
48
|
+
type: 'secret',
|
|
49
|
+
optional: true,
|
|
50
|
+
description: 'The entire service account key file, as JSON or base64-encoded ' +
|
|
51
|
+
'JSON (detected automatically), for supplying the key from an ' +
|
|
52
|
+
'environment variable or secret manager rather than from disk.',
|
|
53
|
+
},
|
|
54
|
+
// A google-auth-library AuthClient, for credentials Malloy cannot build
|
|
55
|
+
// from config text: impersonation, workload identity federation, a proxied
|
|
56
|
+
// or test credential. The config file names an overlay, the host registers
|
|
57
|
+
// it, and the value never appears in the file — which is what `opaque` plus
|
|
58
|
+
// `source: 'overlay'` say. `mustHaveValue` is the safety half: an overlay
|
|
59
|
+
// that isn't registered would otherwise drop the property and quietly fall
|
|
60
|
+
// back to ambient credentials.
|
|
61
|
+
//
|
|
62
|
+
// When one is supplied, the SDK resolves the project id through it rather
|
|
63
|
+
// than through ambient credentials, so set billingProjectId alongside.
|
|
64
|
+
{
|
|
65
|
+
name: 'authClient',
|
|
66
|
+
displayName: 'Auth Client',
|
|
67
|
+
type: 'opaque',
|
|
68
|
+
source: 'overlay',
|
|
69
|
+
mustHaveValue: true,
|
|
70
|
+
optional: true,
|
|
71
|
+
description: 'Not settable from the UI; supplied by the host.',
|
|
72
|
+
},
|
|
37
73
|
{
|
|
38
74
|
name: 'location',
|
|
39
75
|
displayName: 'Location',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@malloydata/db-bigquery",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.432",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"@google-cloud/bigquery": "7.9.4",
|
|
27
27
|
"@google-cloud/common": "5.0.2",
|
|
28
28
|
"@google-cloud/paginator": "5.0.2",
|
|
29
|
-
"@malloydata/malloy": "0.0.
|
|
29
|
+
"@malloydata/malloy": "0.0.432",
|
|
30
30
|
"gaxios": "^4.2.0"
|
|
31
31
|
}
|
|
32
32
|
}
|