@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.
Files changed (40) hide show
  1. package/dist/client/api.d.ts +1537 -0
  2. package/dist/client/api.js +1572 -0
  3. package/dist/client/api.js.map +1 -0
  4. package/dist/client/base.d.ts +66 -0
  5. package/dist/client/base.js +69 -0
  6. package/dist/client/base.js.map +1 -0
  7. package/dist/client/common.d.ts +65 -0
  8. package/dist/client/common.js +147 -0
  9. package/dist/client/common.js.map +1 -0
  10. package/dist/client/configuration.d.ts +91 -0
  11. package/dist/client/configuration.js +50 -0
  12. package/dist/client/configuration.js.map +1 -0
  13. package/dist/client/index.d.ts +15 -0
  14. package/dist/client/index.js +32 -0
  15. package/dist/client/index.js.map +1 -0
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.js +12 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/publisher_connection.d.ts +29 -0
  20. package/dist/publisher_connection.js +119 -0
  21. package/dist/publisher_connection.js.map +1 -0
  22. package/dist/publisher_connection.spec.d.ts +1 -0
  23. package/dist/publisher_connection.spec.js +806 -0
  24. package/dist/publisher_connection.spec.js.map +1 -0
  25. package/openapitools.json +7 -0
  26. package/package.json +32 -0
  27. package/publisher-api-doc.yaml +1026 -0
  28. package/src/client/.openapi-generator/FILES +9 -0
  29. package/src/client/.openapi-generator/VERSION +1 -0
  30. package/src/client/.openapi-generator-ignore +23 -0
  31. package/src/client/api.ts +2342 -0
  32. package/src/client/base.ts +86 -0
  33. package/src/client/common.ts +150 -0
  34. package/src/client/configuration.ts +115 -0
  35. package/src/client/git_push.sh +57 -0
  36. package/src/client/index.ts +19 -0
  37. package/src/index.ts +8 -0
  38. package/src/publisher_connection.spec.ts +1118 -0
  39. package/src/publisher_connection.ts +223 -0
  40. 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
+ }