@malloydata/db-publisher 0.0.270-dev250429163414
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/client/api.d.ts +1537 -0
- package/dist/client/api.js +1572 -0
- package/dist/client/api.js.map +1 -0
- package/dist/client/base.d.ts +66 -0
- package/dist/client/base.js +69 -0
- package/dist/client/base.js.map +1 -0
- package/dist/client/common.d.ts +65 -0
- package/dist/client/common.js +147 -0
- package/dist/client/common.js.map +1 -0
- package/dist/client/configuration.d.ts +91 -0
- package/dist/client/configuration.js +50 -0
- package/dist/client/configuration.js.map +1 -0
- package/dist/client/index.d.ts +15 -0
- package/dist/client/index.js +32 -0
- package/dist/client/index.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/publisher_connection.d.ts +29 -0
- package/dist/publisher_connection.js +119 -0
- package/dist/publisher_connection.js.map +1 -0
- package/dist/publisher_connection.spec.d.ts +1 -0
- package/dist/publisher_connection.spec.js +806 -0
- package/dist/publisher_connection.spec.js.map +1 -0
- package/openapitools.json +7 -0
- package/package.json +32 -0
- package/publisher-api-doc.yaml +1026 -0
- package/src/client/.openapi-generator/FILES +9 -0
- package/src/client/.openapi-generator/VERSION +1 -0
- package/src/client/.openapi-generator-ignore +23 -0
- package/src/client/api.ts +2342 -0
- package/src/client/base.ts +86 -0
- package/src/client/common.ts +150 -0
- package/src/client/configuration.ts +115 -0
- package/src/client/git_push.sh +57 -0
- package/src/client/index.ts +19 -0
- package/src/index.ts +8 -0
- package/src/publisher_connection.spec.ts +1118 -0
- package/src/publisher_connection.ts +223 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,1118 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as malloy from '@malloydata/malloy';
|
|
9
|
+
import {describeIfDatabaseAvailable} from '@malloydata/malloy/test';
|
|
10
|
+
import {PublisherConnection} from './publisher_connection';
|
|
11
|
+
import {fileURLToPath} from 'url';
|
|
12
|
+
import * as util from 'util';
|
|
13
|
+
import * as fs from 'fs';
|
|
14
|
+
import type {ConnectionAttributes} from './client';
|
|
15
|
+
import {Configuration, ConnectionsApi} from './client';
|
|
16
|
+
import type {AxiosResponse} from 'axios';
|
|
17
|
+
import type {
|
|
18
|
+
TableSourceDef,
|
|
19
|
+
SQLSourceDef,
|
|
20
|
+
MalloyQueryData,
|
|
21
|
+
QueryDataRow,
|
|
22
|
+
} from '@malloydata/malloy';
|
|
23
|
+
|
|
24
|
+
// mocks client code for testing
|
|
25
|
+
jest.mock('./client', () => {
|
|
26
|
+
const mockConnectionsApi = {
|
|
27
|
+
getConnection: jest.fn(),
|
|
28
|
+
getTest: jest.fn(),
|
|
29
|
+
getTablesource: jest.fn(),
|
|
30
|
+
getSqlsource: jest.fn(),
|
|
31
|
+
getQuerydata: jest.fn(),
|
|
32
|
+
getTemporarytable: jest.fn(),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
Configuration: jest.fn().mockImplementation(() => ({
|
|
37
|
+
basePath: 'http://test.com/api/v0',
|
|
38
|
+
})),
|
|
39
|
+
ConnectionsApi: jest.fn().mockImplementation(() => mockConnectionsApi),
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const [describe] = describeIfDatabaseAvailable(['publisher']);
|
|
44
|
+
|
|
45
|
+
describe('db:Publisher', () => {
|
|
46
|
+
describe('unit', () => {
|
|
47
|
+
describe('create', () => {
|
|
48
|
+
let mockConnectionsApi: jest.Mocked<ConnectionsApi>;
|
|
49
|
+
let _mockConfiguration: jest.Mocked<Configuration>;
|
|
50
|
+
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
// Get fresh instances of the mocks
|
|
53
|
+
mockConnectionsApi = new ConnectionsApi(
|
|
54
|
+
new Configuration()
|
|
55
|
+
) as jest.Mocked<ConnectionsApi>;
|
|
56
|
+
_mockConfiguration = new Configuration() as jest.Mocked<Configuration>;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
jest.clearAllMocks();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should create a connection successfully', async () => {
|
|
64
|
+
const mockConnectionAttributes: ConnectionAttributes = {
|
|
65
|
+
dialectName: 'bigquery',
|
|
66
|
+
isPool: false,
|
|
67
|
+
canPersist: true,
|
|
68
|
+
canStream: true,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const mockConnectionResponse: AxiosResponse = {
|
|
72
|
+
data: {
|
|
73
|
+
attributes: mockConnectionAttributes,
|
|
74
|
+
},
|
|
75
|
+
status: 200,
|
|
76
|
+
statusText: 'OK',
|
|
77
|
+
headers: {},
|
|
78
|
+
config: {} as AxiosResponse['config'],
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const mockTestResponse: AxiosResponse = {
|
|
82
|
+
data: undefined,
|
|
83
|
+
status: 200,
|
|
84
|
+
statusText: 'OK',
|
|
85
|
+
headers: {},
|
|
86
|
+
config: {} as AxiosResponse['config'],
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
mockConnectionsApi.getConnection.mockResolvedValueOnce(
|
|
90
|
+
mockConnectionResponse
|
|
91
|
+
);
|
|
92
|
+
mockConnectionsApi.getTest.mockResolvedValueOnce(mockTestResponse);
|
|
93
|
+
|
|
94
|
+
const connection = await PublisherConnection.create('test-connection', {
|
|
95
|
+
connectionUri:
|
|
96
|
+
'http://test.com/api/v0/projects/test-project/connections/test-connection',
|
|
97
|
+
accessToken: 'test-token',
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
expect(connection).toBeInstanceOf(PublisherConnection);
|
|
101
|
+
expect(connection.name).toBe('test-connection');
|
|
102
|
+
expect(connection.projectName).toBe('test-project');
|
|
103
|
+
expect(connection.dialectName).toBe('bigquery');
|
|
104
|
+
expect(connection.isPool()).toBe(false);
|
|
105
|
+
expect(connection.canPersist()).toBe(true);
|
|
106
|
+
expect(connection.canStream()).toBe(true);
|
|
107
|
+
expect(mockConnectionsApi.getConnection).toHaveBeenCalledWith(
|
|
108
|
+
'test-project',
|
|
109
|
+
'test-connection',
|
|
110
|
+
{
|
|
111
|
+
headers: {
|
|
112
|
+
Authorization: 'Bearer test-token',
|
|
113
|
+
},
|
|
114
|
+
}
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should throw error for invalid connection URI format', async () => {
|
|
119
|
+
await expect(
|
|
120
|
+
PublisherConnection.create('test-connection', {
|
|
121
|
+
connectionUri: 'http://test.com/invalid/path',
|
|
122
|
+
accessToken: 'test-token',
|
|
123
|
+
})
|
|
124
|
+
).rejects.toThrow('Invalid connection URI');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should throw error for connection name mismatch', async () => {
|
|
128
|
+
await expect(
|
|
129
|
+
PublisherConnection.create('different-name', {
|
|
130
|
+
connectionUri:
|
|
131
|
+
'http://test.com/api/v0/projects/test-project/connections/test-connection',
|
|
132
|
+
accessToken: 'test-token',
|
|
133
|
+
})
|
|
134
|
+
).rejects.toThrow('Connection name mismatch');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should handle no access token', async () => {
|
|
138
|
+
const mockConnectionAttributes: ConnectionAttributes = {
|
|
139
|
+
dialectName: 'bigquery',
|
|
140
|
+
isPool: false,
|
|
141
|
+
canPersist: true,
|
|
142
|
+
canStream: true,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const mockConnectionResponse: AxiosResponse = {
|
|
146
|
+
data: {
|
|
147
|
+
attributes: mockConnectionAttributes,
|
|
148
|
+
},
|
|
149
|
+
status: 200,
|
|
150
|
+
statusText: 'OK',
|
|
151
|
+
headers: {},
|
|
152
|
+
config: {} as AxiosResponse['config'],
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const mockTestResponse: AxiosResponse = {
|
|
156
|
+
data: undefined,
|
|
157
|
+
status: 200,
|
|
158
|
+
statusText: 'OK',
|
|
159
|
+
headers: {},
|
|
160
|
+
config: {} as AxiosResponse['config'],
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
mockConnectionsApi.getConnection.mockResolvedValueOnce(
|
|
164
|
+
mockConnectionResponse
|
|
165
|
+
);
|
|
166
|
+
mockConnectionsApi.getTest.mockResolvedValueOnce(mockTestResponse);
|
|
167
|
+
|
|
168
|
+
const connection = await PublisherConnection.create('test-connection', {
|
|
169
|
+
connectionUri:
|
|
170
|
+
'http://test.com/api/v0/projects/test-project/connections/test-connection',
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
expect(connection).toBeInstanceOf(PublisherConnection);
|
|
174
|
+
expect(mockConnectionsApi.getConnection).toHaveBeenCalledWith(
|
|
175
|
+
'test-project',
|
|
176
|
+
'test-connection',
|
|
177
|
+
{
|
|
178
|
+
headers: {},
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('fetchTableSchema', () => {
|
|
185
|
+
let mockConnectionsApi: jest.Mocked<ConnectionsApi>;
|
|
186
|
+
let _mockConfiguration: jest.Mocked<Configuration>;
|
|
187
|
+
|
|
188
|
+
beforeEach(() => {
|
|
189
|
+
// Get fresh instances of the mocks
|
|
190
|
+
mockConnectionsApi = new ConnectionsApi(
|
|
191
|
+
new Configuration()
|
|
192
|
+
) as jest.Mocked<ConnectionsApi>;
|
|
193
|
+
_mockConfiguration = new Configuration() as jest.Mocked<Configuration>;
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
afterEach(() => {
|
|
197
|
+
jest.clearAllMocks();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should fetch table schema successfully', async () => {
|
|
201
|
+
const mockTableSchema: TableSourceDef = {
|
|
202
|
+
type: 'table',
|
|
203
|
+
name: 'test_table',
|
|
204
|
+
tablePath: 'test_path',
|
|
205
|
+
connection: 'test-connection',
|
|
206
|
+
dialect: 'bigquery',
|
|
207
|
+
fields: [
|
|
208
|
+
{name: 'id', type: 'number'},
|
|
209
|
+
{name: 'name', type: 'string'},
|
|
210
|
+
],
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const mockConnectionResponse: AxiosResponse = {
|
|
214
|
+
data: {
|
|
215
|
+
attributes: {
|
|
216
|
+
dialectName: 'bigquery',
|
|
217
|
+
isPool: false,
|
|
218
|
+
canPersist: true,
|
|
219
|
+
canStream: true,
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
status: 200,
|
|
223
|
+
statusText: 'OK',
|
|
224
|
+
headers: {},
|
|
225
|
+
config: {} as AxiosResponse['config'],
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const mockTableResponse: AxiosResponse = {
|
|
229
|
+
data: {
|
|
230
|
+
source: JSON.stringify(mockTableSchema),
|
|
231
|
+
},
|
|
232
|
+
status: 200,
|
|
233
|
+
statusText: 'OK',
|
|
234
|
+
headers: {},
|
|
235
|
+
config: {} as AxiosResponse['config'],
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
mockConnectionsApi.getConnection.mockResolvedValueOnce(
|
|
239
|
+
mockConnectionResponse
|
|
240
|
+
);
|
|
241
|
+
mockConnectionsApi.getTablesource.mockResolvedValueOnce(
|
|
242
|
+
mockTableResponse
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
const connection = await PublisherConnection.create('test-connection', {
|
|
246
|
+
connectionUri:
|
|
247
|
+
'http://test.com/api/v0/projects/test-project/connections/test-connection',
|
|
248
|
+
accessToken: 'test-token',
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const schema = await connection.fetchTableSchema(
|
|
252
|
+
'test_key',
|
|
253
|
+
'test_path'
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
expect(schema).toEqual(mockTableSchema);
|
|
257
|
+
expect(mockConnectionsApi.getTablesource).toHaveBeenCalledWith(
|
|
258
|
+
'test-project',
|
|
259
|
+
'test-connection',
|
|
260
|
+
'test_key',
|
|
261
|
+
'test_path',
|
|
262
|
+
{
|
|
263
|
+
headers: {
|
|
264
|
+
Authorization: 'Bearer test-token',
|
|
265
|
+
},
|
|
266
|
+
}
|
|
267
|
+
);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('should handle API errors', async () => {
|
|
271
|
+
await setupAndTestApiError(
|
|
272
|
+
mockConnectionsApi,
|
|
273
|
+
'getTablesource',
|
|
274
|
+
connection => connection.fetchTableSchema('test_key', 'test_path')
|
|
275
|
+
);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('should handle invalid JSON response', async () => {
|
|
279
|
+
await setupAndTestInvalidJsonResponse(
|
|
280
|
+
mockConnectionsApi,
|
|
281
|
+
'getTablesource',
|
|
282
|
+
connection => connection.fetchTableSchema('test_key', 'test_path')
|
|
283
|
+
);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
describe('fetchSelectSchema', () => {
|
|
288
|
+
let mockConnectionsApi: jest.Mocked<ConnectionsApi>;
|
|
289
|
+
let _mockConfiguration: jest.Mocked<Configuration>;
|
|
290
|
+
|
|
291
|
+
beforeEach(() => {
|
|
292
|
+
// Get fresh instances of the mocks
|
|
293
|
+
mockConnectionsApi = new ConnectionsApi(
|
|
294
|
+
new Configuration()
|
|
295
|
+
) as jest.Mocked<ConnectionsApi>;
|
|
296
|
+
_mockConfiguration = new Configuration() as jest.Mocked<Configuration>;
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
afterEach(() => {
|
|
300
|
+
jest.clearAllMocks();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('should fetch SQL schema successfully', async () => {
|
|
304
|
+
const mockSqlSchema: SQLSourceDef = {
|
|
305
|
+
type: 'sql_select',
|
|
306
|
+
name: 'test_query',
|
|
307
|
+
selectStr: 'SELECT * FROM test_table',
|
|
308
|
+
connection: 'test-connection',
|
|
309
|
+
dialect: 'bigquery',
|
|
310
|
+
fields: [
|
|
311
|
+
{name: 'id', type: 'number'},
|
|
312
|
+
{name: 'name', type: 'string'},
|
|
313
|
+
],
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const mockConnectionResponse: AxiosResponse = {
|
|
317
|
+
data: {
|
|
318
|
+
attributes: {
|
|
319
|
+
dialectName: 'bigquery',
|
|
320
|
+
isPool: false,
|
|
321
|
+
canPersist: true,
|
|
322
|
+
canStream: true,
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
status: 200,
|
|
326
|
+
statusText: 'OK',
|
|
327
|
+
headers: {},
|
|
328
|
+
config: {} as AxiosResponse['config'],
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const mockSqlResponse: AxiosResponse = {
|
|
332
|
+
data: {
|
|
333
|
+
source: JSON.stringify(mockSqlSchema),
|
|
334
|
+
},
|
|
335
|
+
status: 200,
|
|
336
|
+
statusText: 'OK',
|
|
337
|
+
headers: {},
|
|
338
|
+
config: {} as AxiosResponse['config'],
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
mockConnectionsApi.getConnection.mockResolvedValueOnce(
|
|
342
|
+
mockConnectionResponse
|
|
343
|
+
);
|
|
344
|
+
mockConnectionsApi.getSqlsource.mockResolvedValueOnce(mockSqlResponse);
|
|
345
|
+
|
|
346
|
+
const connection = await PublisherConnection.create('test-connection', {
|
|
347
|
+
connectionUri:
|
|
348
|
+
'http://test.com/api/v0/projects/test-project/connections/test-connection',
|
|
349
|
+
accessToken: 'test-token',
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
const schema = await connection.fetchSelectSchema({
|
|
353
|
+
selectStr: 'SELECT * FROM test_table',
|
|
354
|
+
connection: 'test-connection',
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
expect(schema).toEqual(mockSqlSchema);
|
|
358
|
+
expect(mockConnectionsApi.getSqlsource).toHaveBeenCalledWith(
|
|
359
|
+
'test-project',
|
|
360
|
+
'test-connection',
|
|
361
|
+
'SELECT * FROM test_table',
|
|
362
|
+
{
|
|
363
|
+
headers: {
|
|
364
|
+
Authorization: 'Bearer test-token',
|
|
365
|
+
},
|
|
366
|
+
}
|
|
367
|
+
);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('should handle API errors', async () => {
|
|
371
|
+
await setupAndTestApiError(
|
|
372
|
+
mockConnectionsApi,
|
|
373
|
+
'getSqlsource',
|
|
374
|
+
connection =>
|
|
375
|
+
connection.fetchSelectSchema({
|
|
376
|
+
selectStr: 'SELECT * FROM test_table',
|
|
377
|
+
connection: 'test-connection',
|
|
378
|
+
})
|
|
379
|
+
);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('should handle invalid JSON response', async () => {
|
|
383
|
+
await setupAndTestInvalidJsonResponse(
|
|
384
|
+
mockConnectionsApi,
|
|
385
|
+
'getSqlsource',
|
|
386
|
+
connection =>
|
|
387
|
+
connection.fetchSelectSchema({
|
|
388
|
+
selectStr: 'SELECT * FROM test_table',
|
|
389
|
+
connection: 'test-connection',
|
|
390
|
+
})
|
|
391
|
+
);
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
describe('runSQL', () => {
|
|
396
|
+
let mockConnectionsApi: jest.Mocked<ConnectionsApi>;
|
|
397
|
+
let _mockConfiguration: jest.Mocked<Configuration>;
|
|
398
|
+
|
|
399
|
+
beforeEach(() => {
|
|
400
|
+
// Get fresh instances of the mocks
|
|
401
|
+
mockConnectionsApi = new ConnectionsApi(
|
|
402
|
+
new Configuration()
|
|
403
|
+
) as jest.Mocked<ConnectionsApi>;
|
|
404
|
+
_mockConfiguration = new Configuration() as jest.Mocked<Configuration>;
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
afterEach(() => {
|
|
408
|
+
jest.clearAllMocks();
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('should run SQL query successfully', async () => {
|
|
412
|
+
const mockQueryData: MalloyQueryData = {
|
|
413
|
+
rows: [
|
|
414
|
+
{id: 1, name: 'test1'},
|
|
415
|
+
{id: 2, name: 'test2'},
|
|
416
|
+
],
|
|
417
|
+
totalRows: 2,
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
const mockConnectionResponse: AxiosResponse = {
|
|
421
|
+
data: {
|
|
422
|
+
attributes: {
|
|
423
|
+
dialectName: 'bigquery',
|
|
424
|
+
isPool: false,
|
|
425
|
+
canPersist: true,
|
|
426
|
+
canStream: true,
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
status: 200,
|
|
430
|
+
statusText: 'OK',
|
|
431
|
+
headers: {},
|
|
432
|
+
config: {} as AxiosResponse['config'],
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
const mockQueryResponse: AxiosResponse = {
|
|
436
|
+
data: {
|
|
437
|
+
data: JSON.stringify(mockQueryData),
|
|
438
|
+
},
|
|
439
|
+
status: 200,
|
|
440
|
+
statusText: 'OK',
|
|
441
|
+
headers: {},
|
|
442
|
+
config: {} as AxiosResponse['config'],
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
mockConnectionsApi.getConnection.mockResolvedValueOnce(
|
|
446
|
+
mockConnectionResponse
|
|
447
|
+
);
|
|
448
|
+
mockConnectionsApi.getQuerydata.mockResolvedValueOnce(
|
|
449
|
+
mockQueryResponse
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
const connection = await PublisherConnection.create('test-connection', {
|
|
453
|
+
connectionUri:
|
|
454
|
+
'http://test.com/api/v0/projects/test-project/connections/test-connection',
|
|
455
|
+
accessToken: 'test-token',
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
const result = await connection.runSQL('SELECT * FROM test_table');
|
|
459
|
+
|
|
460
|
+
expect(result).toEqual(mockQueryData);
|
|
461
|
+
expect(mockConnectionsApi.getQuerydata).toHaveBeenCalledWith(
|
|
462
|
+
'test-project',
|
|
463
|
+
'test-connection',
|
|
464
|
+
'SELECT * FROM test_table',
|
|
465
|
+
'{}',
|
|
466
|
+
{
|
|
467
|
+
headers: {
|
|
468
|
+
Authorization: 'Bearer test-token',
|
|
469
|
+
},
|
|
470
|
+
}
|
|
471
|
+
);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it('should run SQL query with options', async () => {
|
|
475
|
+
const mockQueryData: MalloyQueryData = {
|
|
476
|
+
rows: [
|
|
477
|
+
{id: 1, name: 'test1'},
|
|
478
|
+
{id: 2, name: 'test2'},
|
|
479
|
+
],
|
|
480
|
+
totalRows: 2,
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
const mockConnectionResponse: AxiosResponse = {
|
|
484
|
+
data: {
|
|
485
|
+
attributes: {
|
|
486
|
+
dialectName: 'bigquery',
|
|
487
|
+
isPool: false,
|
|
488
|
+
canPersist: true,
|
|
489
|
+
canStream: true,
|
|
490
|
+
},
|
|
491
|
+
},
|
|
492
|
+
status: 200,
|
|
493
|
+
statusText: 'OK',
|
|
494
|
+
headers: {},
|
|
495
|
+
config: {} as AxiosResponse['config'],
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
const mockQueryResponse: AxiosResponse = {
|
|
499
|
+
data: {
|
|
500
|
+
data: JSON.stringify(mockQueryData),
|
|
501
|
+
},
|
|
502
|
+
status: 200,
|
|
503
|
+
statusText: 'OK',
|
|
504
|
+
headers: {},
|
|
505
|
+
config: {} as AxiosResponse['config'],
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
mockConnectionsApi.getConnection.mockResolvedValueOnce(
|
|
509
|
+
mockConnectionResponse
|
|
510
|
+
);
|
|
511
|
+
mockConnectionsApi.getQuerydata.mockResolvedValueOnce(
|
|
512
|
+
mockQueryResponse
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
const connection = await PublisherConnection.create('test-connection', {
|
|
516
|
+
connectionUri:
|
|
517
|
+
'http://test.com/api/v0/projects/test-project/connections/test-connection',
|
|
518
|
+
accessToken: 'test-token',
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
const options = {
|
|
522
|
+
rowLimit: 100,
|
|
523
|
+
timeoutMs: 5000,
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
const result = await connection.runSQL(
|
|
527
|
+
'SELECT * FROM test_table',
|
|
528
|
+
options
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
expect(result).toEqual(mockQueryData);
|
|
532
|
+
expect(mockConnectionsApi.getQuerydata).toHaveBeenCalledWith(
|
|
533
|
+
'test-project',
|
|
534
|
+
'test-connection',
|
|
535
|
+
'SELECT * FROM test_table',
|
|
536
|
+
JSON.stringify(options),
|
|
537
|
+
{
|
|
538
|
+
headers: {
|
|
539
|
+
Authorization: 'Bearer test-token',
|
|
540
|
+
},
|
|
541
|
+
}
|
|
542
|
+
);
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it('should handle API errors', async () => {
|
|
546
|
+
await setupAndTestApiError(
|
|
547
|
+
mockConnectionsApi,
|
|
548
|
+
'getQuerydata',
|
|
549
|
+
connection => connection.runSQL('SELECT * FROM test_table')
|
|
550
|
+
);
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
it('should handle invalid JSON response', async () => {
|
|
554
|
+
await setupAndTestInvalidJsonResponse(
|
|
555
|
+
mockConnectionsApi,
|
|
556
|
+
'getQuerydata',
|
|
557
|
+
connection => connection.runSQL('SELECT * FROM test_table'),
|
|
558
|
+
{data: 'invalid json'}
|
|
559
|
+
);
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
describe('runSQLStream', () => {
|
|
564
|
+
let mockConnectionsApi: jest.Mocked<ConnectionsApi>;
|
|
565
|
+
let _mockConfiguration: jest.Mocked<Configuration>;
|
|
566
|
+
|
|
567
|
+
beforeEach(() => {
|
|
568
|
+
// Get fresh instances of the mocks
|
|
569
|
+
mockConnectionsApi = new ConnectionsApi(
|
|
570
|
+
new Configuration()
|
|
571
|
+
) as jest.Mocked<ConnectionsApi>;
|
|
572
|
+
_mockConfiguration = new Configuration() as jest.Mocked<Configuration>;
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
afterEach(() => {
|
|
576
|
+
jest.clearAllMocks();
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
it('should stream SQL query results successfully', async () => {
|
|
580
|
+
const mockQueryData: MalloyQueryData = {
|
|
581
|
+
rows: [
|
|
582
|
+
{id: 1, name: 'test1'},
|
|
583
|
+
{id: 2, name: 'test2'},
|
|
584
|
+
],
|
|
585
|
+
totalRows: 2,
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
const mockConnectionResponse: AxiosResponse = {
|
|
589
|
+
data: {
|
|
590
|
+
attributes: {
|
|
591
|
+
dialectName: 'bigquery',
|
|
592
|
+
isPool: false,
|
|
593
|
+
canPersist: true,
|
|
594
|
+
canStream: true,
|
|
595
|
+
},
|
|
596
|
+
},
|
|
597
|
+
status: 200,
|
|
598
|
+
statusText: 'OK',
|
|
599
|
+
headers: {},
|
|
600
|
+
config: {} as AxiosResponse['config'],
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
const mockQueryResponse: AxiosResponse = {
|
|
604
|
+
data: {
|
|
605
|
+
data: JSON.stringify(mockQueryData),
|
|
606
|
+
},
|
|
607
|
+
status: 200,
|
|
608
|
+
statusText: 'OK',
|
|
609
|
+
headers: {},
|
|
610
|
+
config: {} as AxiosResponse['config'],
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
mockConnectionsApi.getConnection.mockResolvedValueOnce(
|
|
614
|
+
mockConnectionResponse
|
|
615
|
+
);
|
|
616
|
+
mockConnectionsApi.getQuerydata.mockResolvedValueOnce(
|
|
617
|
+
mockQueryResponse
|
|
618
|
+
);
|
|
619
|
+
|
|
620
|
+
const connection = await PublisherConnection.create('test-connection', {
|
|
621
|
+
connectionUri:
|
|
622
|
+
'http://test.com/api/v0/projects/test-project/connections/test-connection',
|
|
623
|
+
accessToken: 'test-token',
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
const stream = connection.runSQLStream('SELECT * FROM test_table');
|
|
627
|
+
const results: QueryDataRow[] = [];
|
|
628
|
+
|
|
629
|
+
for await (const row of stream) {
|
|
630
|
+
results.push(row);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
expect(results).toEqual(mockQueryData.rows);
|
|
634
|
+
expect(mockConnectionsApi.getQuerydata).toHaveBeenCalledWith(
|
|
635
|
+
'test-project',
|
|
636
|
+
'test-connection',
|
|
637
|
+
'SELECT * FROM test_table',
|
|
638
|
+
'{}',
|
|
639
|
+
{
|
|
640
|
+
headers: {
|
|
641
|
+
Authorization: 'Bearer test-token',
|
|
642
|
+
},
|
|
643
|
+
}
|
|
644
|
+
);
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
it('should stream SQL query results with options', async () => {
|
|
648
|
+
const mockQueryData: MalloyQueryData = {
|
|
649
|
+
rows: [
|
|
650
|
+
{id: 1, name: 'test1'},
|
|
651
|
+
{id: 2, name: 'test2'},
|
|
652
|
+
],
|
|
653
|
+
totalRows: 2,
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
const mockConnectionResponse: AxiosResponse = {
|
|
657
|
+
data: {
|
|
658
|
+
attributes: {
|
|
659
|
+
dialectName: 'bigquery',
|
|
660
|
+
isPool: false,
|
|
661
|
+
canPersist: true,
|
|
662
|
+
canStream: true,
|
|
663
|
+
},
|
|
664
|
+
},
|
|
665
|
+
status: 200,
|
|
666
|
+
statusText: 'OK',
|
|
667
|
+
headers: {},
|
|
668
|
+
config: {} as AxiosResponse['config'],
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
const mockQueryResponse: AxiosResponse = {
|
|
672
|
+
data: {
|
|
673
|
+
data: JSON.stringify(mockQueryData),
|
|
674
|
+
},
|
|
675
|
+
status: 200,
|
|
676
|
+
statusText: 'OK',
|
|
677
|
+
headers: {},
|
|
678
|
+
config: {} as AxiosResponse['config'],
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
mockConnectionsApi.getConnection.mockResolvedValueOnce(
|
|
682
|
+
mockConnectionResponse
|
|
683
|
+
);
|
|
684
|
+
mockConnectionsApi.getQuerydata.mockResolvedValueOnce(
|
|
685
|
+
mockQueryResponse
|
|
686
|
+
);
|
|
687
|
+
|
|
688
|
+
const connection = await PublisherConnection.create('test-connection', {
|
|
689
|
+
connectionUri:
|
|
690
|
+
'http://test.com/api/v0/projects/test-project/connections/test-connection',
|
|
691
|
+
accessToken: 'test-token',
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
const options = {
|
|
695
|
+
rowLimit: 100,
|
|
696
|
+
timeoutMs: 5000,
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
const stream = connection.runSQLStream(
|
|
700
|
+
'SELECT * FROM test_table',
|
|
701
|
+
options
|
|
702
|
+
);
|
|
703
|
+
const results: QueryDataRow[] = [];
|
|
704
|
+
|
|
705
|
+
for await (const row of stream) {
|
|
706
|
+
results.push(row);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
expect(results).toEqual(mockQueryData.rows);
|
|
710
|
+
expect(mockConnectionsApi.getQuerydata).toHaveBeenCalledWith(
|
|
711
|
+
'test-project',
|
|
712
|
+
'test-connection',
|
|
713
|
+
'SELECT * FROM test_table',
|
|
714
|
+
JSON.stringify(options),
|
|
715
|
+
{
|
|
716
|
+
headers: {
|
|
717
|
+
Authorization: 'Bearer test-token',
|
|
718
|
+
},
|
|
719
|
+
}
|
|
720
|
+
);
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
it('should handle API errors', async () => {
|
|
724
|
+
await setupAndTestApiError(
|
|
725
|
+
mockConnectionsApi,
|
|
726
|
+
'getQuerydata',
|
|
727
|
+
async connection => {
|
|
728
|
+
const stream = connection.runSQLStream('SELECT * FROM test_table');
|
|
729
|
+
const results: QueryDataRow[] = [];
|
|
730
|
+
for await (const row of stream) {
|
|
731
|
+
results.push(row);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
);
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
it('should handle invalid JSON response', async () => {
|
|
738
|
+
await setupAndTestInvalidJsonResponse(
|
|
739
|
+
mockConnectionsApi,
|
|
740
|
+
'getQuerydata',
|
|
741
|
+
async connection => {
|
|
742
|
+
const stream = connection.runSQLStream('SELECT * FROM test_table');
|
|
743
|
+
const results: QueryDataRow[] = [];
|
|
744
|
+
for await (const row of stream) {
|
|
745
|
+
results.push(row);
|
|
746
|
+
}
|
|
747
|
+
},
|
|
748
|
+
{data: 'invalid json'}
|
|
749
|
+
);
|
|
750
|
+
});
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
describe('manifestTemporaryTable', () => {
|
|
754
|
+
let mockConnectionsApi: jest.Mocked<ConnectionsApi>;
|
|
755
|
+
let _mockConfiguration: jest.Mocked<Configuration>;
|
|
756
|
+
|
|
757
|
+
beforeEach(() => {
|
|
758
|
+
// Get fresh instances of the mocks
|
|
759
|
+
mockConnectionsApi = new ConnectionsApi(
|
|
760
|
+
new Configuration()
|
|
761
|
+
) as jest.Mocked<ConnectionsApi>;
|
|
762
|
+
_mockConfiguration = new Configuration() as jest.Mocked<Configuration>;
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
afterEach(() => {
|
|
766
|
+
jest.clearAllMocks();
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
it('should create temporary table successfully', async () => {
|
|
770
|
+
const mockConnectionResponse: AxiosResponse = {
|
|
771
|
+
data: {
|
|
772
|
+
attributes: {
|
|
773
|
+
dialectName: 'bigquery',
|
|
774
|
+
isPool: false,
|
|
775
|
+
canPersist: true,
|
|
776
|
+
canStream: true,
|
|
777
|
+
},
|
|
778
|
+
},
|
|
779
|
+
status: 200,
|
|
780
|
+
statusText: 'OK',
|
|
781
|
+
headers: {},
|
|
782
|
+
config: {} as AxiosResponse['config'],
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
const mockTableResponse: AxiosResponse = {
|
|
786
|
+
data: {
|
|
787
|
+
table: 'temp_table_123',
|
|
788
|
+
},
|
|
789
|
+
status: 200,
|
|
790
|
+
statusText: 'OK',
|
|
791
|
+
headers: {},
|
|
792
|
+
config: {} as AxiosResponse['config'],
|
|
793
|
+
};
|
|
794
|
+
|
|
795
|
+
mockConnectionsApi.getConnection.mockResolvedValueOnce(
|
|
796
|
+
mockConnectionResponse
|
|
797
|
+
);
|
|
798
|
+
mockConnectionsApi.getTemporarytable.mockResolvedValueOnce(
|
|
799
|
+
mockTableResponse
|
|
800
|
+
);
|
|
801
|
+
|
|
802
|
+
const connection = await PublisherConnection.create('test-connection', {
|
|
803
|
+
connectionUri:
|
|
804
|
+
'http://test.com/api/v0/projects/test-project/connections/test-connection',
|
|
805
|
+
accessToken: 'test-token',
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
const tableName = await connection.manifestTemporaryTable(
|
|
809
|
+
'SELECT * FROM test_table'
|
|
810
|
+
);
|
|
811
|
+
|
|
812
|
+
expect(tableName).toBe('temp_table_123');
|
|
813
|
+
expect(mockConnectionsApi.getTemporarytable).toHaveBeenCalledWith(
|
|
814
|
+
'test-project',
|
|
815
|
+
'test-connection',
|
|
816
|
+
'SELECT * FROM test_table',
|
|
817
|
+
{
|
|
818
|
+
headers: {
|
|
819
|
+
Authorization: 'Bearer test-token',
|
|
820
|
+
},
|
|
821
|
+
}
|
|
822
|
+
);
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
it('should handle API errors', async () => {
|
|
826
|
+
await setupAndTestApiError(
|
|
827
|
+
mockConnectionsApi,
|
|
828
|
+
'getTemporarytable',
|
|
829
|
+
connection =>
|
|
830
|
+
connection.manifestTemporaryTable('SELECT * FROM test_table')
|
|
831
|
+
);
|
|
832
|
+
});
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
describe('test', () => {
|
|
836
|
+
let mockConnectionsApi: jest.Mocked<ConnectionsApi>;
|
|
837
|
+
let _mockConfiguration: jest.Mocked<Configuration>;
|
|
838
|
+
|
|
839
|
+
beforeEach(() => {
|
|
840
|
+
// Get fresh instances of the mocks
|
|
841
|
+
mockConnectionsApi = new ConnectionsApi(
|
|
842
|
+
new Configuration()
|
|
843
|
+
) as jest.Mocked<ConnectionsApi>;
|
|
844
|
+
_mockConfiguration = new Configuration() as jest.Mocked<Configuration>;
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
afterEach(() => {
|
|
848
|
+
jest.clearAllMocks();
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
it('should test connection successfully', async () => {
|
|
852
|
+
const mockConnectionResponse: AxiosResponse = {
|
|
853
|
+
data: {
|
|
854
|
+
attributes: {
|
|
855
|
+
dialectName: 'bigquery',
|
|
856
|
+
isPool: false,
|
|
857
|
+
canPersist: true,
|
|
858
|
+
canStream: true,
|
|
859
|
+
},
|
|
860
|
+
},
|
|
861
|
+
status: 200,
|
|
862
|
+
statusText: 'OK',
|
|
863
|
+
headers: {},
|
|
864
|
+
config: {} as AxiosResponse['config'],
|
|
865
|
+
};
|
|
866
|
+
|
|
867
|
+
const mockTestResponse: AxiosResponse = {
|
|
868
|
+
data: undefined,
|
|
869
|
+
status: 200,
|
|
870
|
+
statusText: 'OK',
|
|
871
|
+
headers: {},
|
|
872
|
+
config: {} as AxiosResponse['config'],
|
|
873
|
+
};
|
|
874
|
+
|
|
875
|
+
mockConnectionsApi.getConnection.mockResolvedValueOnce(
|
|
876
|
+
mockConnectionResponse
|
|
877
|
+
);
|
|
878
|
+
mockConnectionsApi.getTest.mockResolvedValueOnce(mockTestResponse);
|
|
879
|
+
|
|
880
|
+
const connection = await PublisherConnection.create('test-connection', {
|
|
881
|
+
connectionUri:
|
|
882
|
+
'http://test.com/api/v0/projects/test-project/connections/test-connection',
|
|
883
|
+
accessToken: 'test-token',
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
await expect(connection.test()).resolves.not.toThrow();
|
|
887
|
+
expect(mockConnectionsApi.getTest).toHaveBeenCalledWith(
|
|
888
|
+
'test-project',
|
|
889
|
+
'test-connection',
|
|
890
|
+
{
|
|
891
|
+
headers: {
|
|
892
|
+
Authorization: 'Bearer test-token',
|
|
893
|
+
},
|
|
894
|
+
}
|
|
895
|
+
);
|
|
896
|
+
});
|
|
897
|
+
});
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
describe.skip('integration', () => {
|
|
901
|
+
let conn: PublisherConnection;
|
|
902
|
+
let runtime: malloy.Runtime;
|
|
903
|
+
|
|
904
|
+
beforeEach(async () => {
|
|
905
|
+
conn = await PublisherConnection.create(
|
|
906
|
+
'bigquery',
|
|
907
|
+
//{
|
|
908
|
+
//connectionUri: 'http://localhost:4000/api/v0/projects/malloy-samples/connections/bigquery',
|
|
909
|
+
//}
|
|
910
|
+
{
|
|
911
|
+
connectionUri:
|
|
912
|
+
'http://demo.data.pathways.localhost:8000/api/v0/projects/malloy-samples/connections/bigquery',
|
|
913
|
+
accessToken: 'xyz',
|
|
914
|
+
}
|
|
915
|
+
);
|
|
916
|
+
const files = {
|
|
917
|
+
readURL: async (url: URL) => {
|
|
918
|
+
const filePath = fileURLToPath(url);
|
|
919
|
+
return await util.promisify(fs.readFile)(filePath, 'utf8');
|
|
920
|
+
},
|
|
921
|
+
};
|
|
922
|
+
runtime = new malloy.Runtime({
|
|
923
|
+
urlReader: files,
|
|
924
|
+
connection: conn,
|
|
925
|
+
});
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
afterEach(async () => {
|
|
929
|
+
await conn.close();
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
it('tests the connection', async () => {
|
|
933
|
+
await conn.test();
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
it('correctly identifies the dialect', () => {
|
|
937
|
+
expect(conn.dialectName).toBe('standardsql');
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
it('correctly identifies the connection as a pooled connection', () => {
|
|
941
|
+
expect(conn.isPool()).toBe(false);
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
it('correctly identifies the connection as a streaming connection', () => {
|
|
945
|
+
expect(conn.canStream()).toBe(true);
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
it('correctly identifies the connection as a persistSQLResults connection', () => {
|
|
949
|
+
expect(conn.canPersist()).toBe(true);
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
it('fetches the table schema', async () => {
|
|
953
|
+
const schema = await conn.fetchTableSchema(
|
|
954
|
+
'bigquery',
|
|
955
|
+
'bigquery-public-data.hacker_news.full'
|
|
956
|
+
);
|
|
957
|
+
expect(schema.type).toBe('table');
|
|
958
|
+
expect(schema.dialect).toBe('standardsql');
|
|
959
|
+
expect(schema.tablePath).toBe('bigquery-public-data.hacker_news.full');
|
|
960
|
+
expect(schema.fields.length).toBe(14);
|
|
961
|
+
expect(schema.fields[0].name).toBe('title');
|
|
962
|
+
expect(schema.fields[0].type).toBe('string');
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
it('fetches the sql source schema', async () => {
|
|
966
|
+
const schema = await conn.fetchSelectSchema({
|
|
967
|
+
connection: 'bigquery',
|
|
968
|
+
selectStr: 'SELECT * FROM bigquery-public-data.hacker_news.full',
|
|
969
|
+
});
|
|
970
|
+
expect(schema.type).toBe('sql_select');
|
|
971
|
+
expect(schema.dialect).toBe('standardsql');
|
|
972
|
+
expect(schema.fields.length).toBe(14);
|
|
973
|
+
expect(schema.fields[0].name).toBe('title');
|
|
974
|
+
expect(schema.fields[0].type).toBe('string');
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
it('runs a SQL query', async () => {
|
|
978
|
+
const res = await conn.runSQL('SELECT 1 as T');
|
|
979
|
+
expect(res.rows[0]['T']).toBe(1);
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
it('runs a Malloy query', async () => {
|
|
983
|
+
const sql = await runtime
|
|
984
|
+
.loadModel(
|
|
985
|
+
"source: stories is bigquery.table('bigquery-public-data.hacker_news.full')"
|
|
986
|
+
)
|
|
987
|
+
.loadQuery(
|
|
988
|
+
'run: stories -> { aggregate: cnt is count() group_by: `by` order_by: cnt desc limit: 10 }'
|
|
989
|
+
)
|
|
990
|
+
.getSQL();
|
|
991
|
+
const res = await conn.runSQL(sql);
|
|
992
|
+
expect(res.totalRows).toBe(10);
|
|
993
|
+
let total = 0;
|
|
994
|
+
for (const row of res.rows) {
|
|
995
|
+
total += +(row['cnt'] ?? 0);
|
|
996
|
+
}
|
|
997
|
+
expect(total).toBe(1836679);
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
it('runs a Malloy query on an sql source', async () => {
|
|
1001
|
+
const sql = await runtime
|
|
1002
|
+
.loadModel(
|
|
1003
|
+
"source: stories is bigquery.sql('SELECT * FROM bigquery-public-data.hacker_news.full')"
|
|
1004
|
+
)
|
|
1005
|
+
.loadQuery(
|
|
1006
|
+
'run: stories -> { aggregate: cnt is count() group_by: `by` order_by: cnt desc limit: 20 }'
|
|
1007
|
+
)
|
|
1008
|
+
.getSQL();
|
|
1009
|
+
const res = await conn.runSQL(sql);
|
|
1010
|
+
expect(res.totalRows).toBe(20);
|
|
1011
|
+
expect(res.rows[0]['cnt']).toBe(1346912);
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
it('get temporary table name', async () => {
|
|
1015
|
+
const sql = 'SELECT 1 as T';
|
|
1016
|
+
const tempTableName = await conn.manifestTemporaryTable(sql);
|
|
1017
|
+
expect(tempTableName).toBeDefined();
|
|
1018
|
+
expect(tempTableName.startsWith('lofty-complex-452701')).toBe(true);
|
|
1019
|
+
});
|
|
1020
|
+
});
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
// helper function for handling API errors test cases
|
|
1024
|
+
async function testErrorHandling(
|
|
1025
|
+
connection: PublisherConnection,
|
|
1026
|
+
operation: () => Promise<unknown>,
|
|
1027
|
+
errorMessage?: string
|
|
1028
|
+
) {
|
|
1029
|
+
if (errorMessage) {
|
|
1030
|
+
await expect(operation()).rejects.toThrow(errorMessage);
|
|
1031
|
+
} else {
|
|
1032
|
+
await expect(operation()).rejects.toThrow();
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// handles API errors test cases
|
|
1037
|
+
async function setupAndTestApiError(
|
|
1038
|
+
mockConnectionsApi: jest.Mocked<ConnectionsApi>,
|
|
1039
|
+
apiMethod: keyof jest.Mocked<ConnectionsApi>,
|
|
1040
|
+
operation: (connection: PublisherConnection) => Promise<unknown>,
|
|
1041
|
+
errorMessage = 'API Error'
|
|
1042
|
+
) {
|
|
1043
|
+
const mockConnectionResponse: AxiosResponse = {
|
|
1044
|
+
data: {
|
|
1045
|
+
attributes: {
|
|
1046
|
+
dialectName: 'bigquery',
|
|
1047
|
+
isPool: false,
|
|
1048
|
+
canPersist: true,
|
|
1049
|
+
canStream: true,
|
|
1050
|
+
},
|
|
1051
|
+
},
|
|
1052
|
+
status: 200,
|
|
1053
|
+
statusText: 'OK',
|
|
1054
|
+
headers: {},
|
|
1055
|
+
config: {} as AxiosResponse['config'],
|
|
1056
|
+
};
|
|
1057
|
+
|
|
1058
|
+
mockConnectionsApi.getConnection.mockResolvedValueOnce(
|
|
1059
|
+
mockConnectionResponse
|
|
1060
|
+
);
|
|
1061
|
+
mockConnectionsApi[apiMethod].mockRejectedValueOnce(new Error(errorMessage));
|
|
1062
|
+
|
|
1063
|
+
const connection = await PublisherConnection.create('test-connection', {
|
|
1064
|
+
connectionUri:
|
|
1065
|
+
'http://test.com/api/v0/projects/test-project/connections/test-connection',
|
|
1066
|
+
accessToken: 'test-token',
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
await testErrorHandling(
|
|
1070
|
+
connection,
|
|
1071
|
+
() => operation(connection),
|
|
1072
|
+
errorMessage
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// handles invalid JSON response test cases
|
|
1077
|
+
async function setupAndTestInvalidJsonResponse(
|
|
1078
|
+
mockConnectionsApi: jest.Mocked<ConnectionsApi>,
|
|
1079
|
+
apiMethod: keyof jest.Mocked<ConnectionsApi>,
|
|
1080
|
+
operation: (connection: PublisherConnection) => Promise<unknown>,
|
|
1081
|
+
responseData: Record<string, unknown> = {source: 'invalid json'}
|
|
1082
|
+
) {
|
|
1083
|
+
const mockConnectionResponse: AxiosResponse = {
|
|
1084
|
+
data: {
|
|
1085
|
+
attributes: {
|
|
1086
|
+
dialectName: 'bigquery',
|
|
1087
|
+
isPool: false,
|
|
1088
|
+
canPersist: true,
|
|
1089
|
+
canStream: true,
|
|
1090
|
+
},
|
|
1091
|
+
},
|
|
1092
|
+
status: 200,
|
|
1093
|
+
statusText: 'OK',
|
|
1094
|
+
headers: {},
|
|
1095
|
+
config: {} as AxiosResponse['config'],
|
|
1096
|
+
};
|
|
1097
|
+
|
|
1098
|
+
const mockInvalidResponse: AxiosResponse = {
|
|
1099
|
+
data: responseData,
|
|
1100
|
+
status: 200,
|
|
1101
|
+
statusText: 'OK',
|
|
1102
|
+
headers: {},
|
|
1103
|
+
config: {} as AxiosResponse['config'],
|
|
1104
|
+
};
|
|
1105
|
+
|
|
1106
|
+
mockConnectionsApi.getConnection.mockResolvedValueOnce(
|
|
1107
|
+
mockConnectionResponse
|
|
1108
|
+
);
|
|
1109
|
+
mockConnectionsApi[apiMethod].mockResolvedValueOnce(mockInvalidResponse);
|
|
1110
|
+
|
|
1111
|
+
const connection = await PublisherConnection.create('test-connection', {
|
|
1112
|
+
connectionUri:
|
|
1113
|
+
'http://test.com/api/v0/projects/test-project/connections/test-connection',
|
|
1114
|
+
accessToken: 'test-token',
|
|
1115
|
+
});
|
|
1116
|
+
|
|
1117
|
+
await testErrorHandling(connection, () => operation(connection));
|
|
1118
|
+
}
|