@lightdash/cli 1.251.6 → 1.252.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.
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const tslib_1 = require("tslib");
4
4
  const common_1 = require("@lightdash/common");
5
5
  const promises_1 = tslib_1.__importDefault(require("fs/promises"));
6
+ const node_fetch_1 = require("node-fetch");
6
7
  const os_1 = tslib_1.__importDefault(require("os"));
7
8
  const path_1 = tslib_1.__importDefault(require("path"));
8
9
  const context_1 = require("../dbt/context");
@@ -10,6 +11,7 @@ const manifest_1 = require("../dbt/manifest");
10
11
  const validation_1 = require("../dbt/validation");
11
12
  const loader_1 = require("../lightdash/loader");
12
13
  const compile_1 = require("./compile");
14
+ const apiClient_1 = require("./dbt/apiClient");
13
15
  const compile_2 = require("./dbt/compile");
14
16
  const getDbtVersion_1 = require("./dbt/getDbtVersion");
15
17
  vi.mock('../analytics/analytics');
@@ -26,7 +28,11 @@ vi.mock('../dbt/validation');
26
28
  vi.mock('../lightdash/loader');
27
29
  vi.mock('./dbt/compile');
28
30
  vi.mock('./dbt/getDbtVersion');
29
- const dbtNode = (uniqueId, resourceType, compiled) => ({
31
+ vi.mock('./dbt/apiClient', async (importOriginal) => ({
32
+ ...(await importOriginal()),
33
+ lightdashRawApi: vi.fn(),
34
+ }));
35
+ const dbtNode = (uniqueId, resourceType, compiled, extra = {}) => ({
30
36
  unique_id: uniqueId,
31
37
  name: uniqueId.split('.').at(-1) ?? uniqueId,
32
38
  resource_type: resourceType,
@@ -57,6 +63,7 @@ const dbtNode = (uniqueId, resourceType, compiled) => ({
57
63
  relation_name: `"db"."public"."${uniqueId}"`,
58
64
  config: { materialized: 'table' },
59
65
  compiled,
66
+ ...extra,
60
67
  });
61
68
  const manifest = (nodes) => ({
62
69
  metadata: {
@@ -125,6 +132,7 @@ describe('compileProject completeness', () => {
125
132
  skipped: [],
126
133
  }));
127
134
  vi.spyOn(console, 'error').mockImplementation(() => undefined);
135
+ vi.spyOn(console, 'info').mockImplementation(() => undefined);
128
136
  });
129
137
  afterEach(async () => {
130
138
  vi.restoreAllMocks();
@@ -173,7 +181,383 @@ describe('compileProject completeness', () => {
173
181
  const result = await (0, compile_1.compileProject)({
174
182
  ...compileOptions(tempDir),
175
183
  combineManifest: 'external-manifest.json',
184
+ combineManifestProjectUuid: 'project-uuid',
185
+ });
186
+ expect(result.isProjectComplete).toBe(true);
187
+ expect(result.explores.map((explore) => explore.name)).toEqual([
188
+ 'orders',
189
+ 'compiled',
190
+ ]);
191
+ expect(apiClient_1.lightdashRawApi).not.toHaveBeenCalled();
192
+ expect(console.info).toHaveBeenCalledWith(expect.stringContaining('Combined external manifest from'));
193
+ });
194
+ test('projects, merges, and compiles served-only models while preserving local precedence and source identity', async () => {
195
+ const projectManifest = manifest({
196
+ 'model.test.orders': dbtNode('model.test.orders', 'model', true, {
197
+ description: 'Local orders description',
198
+ }),
199
+ });
200
+ const servedManifest = (0, common_1.projectMergedManifest)(manifest({
201
+ 'model.test.orders': dbtNode('model.test.orders', 'model', true, {
202
+ description: 'Served orders description',
203
+ lightdash_source_name: 'local_source',
204
+ }),
205
+ 'model.served.customers': dbtNode('model.served.customers', 'model', true, {
206
+ package_name: 'served',
207
+ lightdash_source_name: 'served_source',
208
+ }),
209
+ 'model.served.customer_helper': dbtNode('model.served.customer_helper', 'model', false, {
210
+ package_name: 'served',
211
+ lightdash_source_name: 'served_source',
212
+ }),
213
+ }));
214
+ vi.mocked(manifest_1.loadManifest).mockResolvedValue(projectManifest);
215
+ vi.mocked(apiClient_1.lightdashRawApi).mockResolvedValue(new node_fetch_1.Response(JSON.stringify(servedManifest)));
216
+ vi.mocked(compile_2.maybeCompileModelsAndJoins).mockResolvedValue({
217
+ compiledModelIds: ['model.test.orders'],
218
+ originallySelectedModelIds: undefined,
219
+ });
220
+ const result = await (0, compile_1.compileProject)({
221
+ ...compileOptions(tempDir),
222
+ combineManifestProjectUuid: 'project-uuid',
223
+ });
224
+ expect(result.explores.map((explore) => explore.name)).toEqual([
225
+ 'orders',
226
+ 'customers',
227
+ ]);
228
+ expect(result.isProjectComplete).toBe(true);
229
+ expect(apiClient_1.lightdashRawApi).toHaveBeenCalledWith({
230
+ method: 'GET',
231
+ url: '/api/v1/projects/project-uuid/dbt/manifest',
232
+ body: undefined,
233
+ });
234
+ expect(console.info).toHaveBeenCalledWith(expect.stringMatching(/added [1-9]\d* model\(s\)/));
235
+ const modelsForValidation = vi.mocked(validation_1.validateDbtModel).mock.calls[0][2];
236
+ const localModel = modelsForValidation.find((model) => model.unique_id === 'model.test.orders');
237
+ expect(localModel.description).toBe('Local orders description');
238
+ expect(localModel.lightdash_source_name).toBe('local_source');
239
+ expect(modelsForValidation.map((model) => model.unique_id)).not.toContain('model.served.customer_helper');
240
+ expect(vi.mocked(validation_1.validateDbtModel).mock.calls[0][3]).toEqual(new Set(['model.served.customers']));
241
+ });
242
+ test('replaces the complete local source while retaining served models from other sources', async () => {
243
+ const projectManifest = manifest({
244
+ 'model.local.orders': dbtNode('model.local.orders', 'model', true, {
245
+ description: 'Local orders description',
246
+ }),
247
+ });
248
+ const servedManifest = (0, common_1.projectMergedManifest)(manifest({
249
+ 'model.local.orders': dbtNode('model.local.orders', 'model', true, {
250
+ description: 'Served orders description',
251
+ lightdash_source_name: 'local_source',
252
+ }),
253
+ 'model.local.deleted': dbtNode('model.local.deleted', 'model', true, {
254
+ lightdash_source_name: 'local_source',
255
+ }),
256
+ 'model.other.customers': dbtNode('model.other.customers', 'model', true, {
257
+ lightdash_source_name: 'other_source',
258
+ }),
259
+ }));
260
+ vi.mocked(manifest_1.loadManifest).mockResolvedValue(projectManifest);
261
+ vi.mocked(apiClient_1.lightdashRawApi).mockResolvedValue(new node_fetch_1.Response(JSON.stringify(servedManifest)));
262
+ vi.mocked(compile_2.maybeCompileModelsAndJoins).mockResolvedValue({
263
+ compiledModelIds: ['model.local.orders'],
264
+ originallySelectedModelIds: undefined,
265
+ });
266
+ const result = await (0, compile_1.compileProject)({
267
+ ...compileOptions(tempDir),
268
+ combineManifestProjectUuid: 'project-uuid',
176
269
  });
177
270
  expect(result.isProjectComplete).toBe(true);
271
+ expect(result.explores.map((explore) => explore.name)).toEqual([
272
+ 'orders',
273
+ 'customers',
274
+ ]);
275
+ const modelsForValidation = vi.mocked(validation_1.validateDbtModel).mock.calls[0][2];
276
+ expect(modelsForValidation).toEqual(expect.arrayContaining([
277
+ expect.objectContaining({
278
+ unique_id: 'model.local.orders',
279
+ description: 'Local orders description',
280
+ lightdash_source_name: 'local_source',
281
+ }),
282
+ expect.objectContaining({
283
+ unique_id: 'model.other.customers',
284
+ lightdash_source_name: 'other_source',
285
+ }),
286
+ ]));
287
+ expect(modelsForValidation.map((model) => model.unique_id)).not.toContain('model.local.deleted');
288
+ });
289
+ test('additively combines selective local models with compiled served collision candidates', async () => {
290
+ const projectManifest = manifest({
291
+ 'model.source_a.orders': dbtNode('model.source_a.orders', 'model', true, {
292
+ description: 'Local selected orders',
293
+ }),
294
+ 'model.source_a.customers': dbtNode('model.source_a.customers', 'model', true, {
295
+ description: 'Local unselected customers',
296
+ }),
297
+ });
298
+ const servedManifest = (0, common_1.projectMergedManifest)(manifest({
299
+ 'model.source_a.orders': dbtNode('model.source_a.orders', 'model', true, {
300
+ description: 'Served selected orders',
301
+ lightdash_source_name: 'source_a',
302
+ }),
303
+ 'model.source_a.customers': dbtNode('model.source_a.customers', 'model', true, {
304
+ description: 'Served unselected customers',
305
+ lightdash_source_name: 'source_a',
306
+ }),
307
+ 'model.source_a.deleted': dbtNode('model.source_a.deleted', 'model', true, {
308
+ lightdash_source_name: 'source_a',
309
+ }),
310
+ 'model.source_b.customers': dbtNode('model.source_b.customers', 'model', true, {
311
+ lightdash_source_name: 'source_b',
312
+ }),
313
+ }));
314
+ vi.mocked(manifest_1.loadManifest).mockResolvedValue(projectManifest);
315
+ vi.mocked(apiClient_1.lightdashRawApi).mockResolvedValue(new node_fetch_1.Response(JSON.stringify(servedManifest)));
316
+ vi.mocked(compile_2.maybeCompileModelsAndJoins).mockResolvedValue({
317
+ compiledModelIds: ['model.source_a.orders'],
318
+ originallySelectedModelIds: ['model.source_a.orders'],
319
+ });
320
+ const result = await (0, compile_1.compileProject)({
321
+ ...compileOptions(tempDir),
322
+ combineManifestProjectUuid: 'project-uuid',
323
+ });
324
+ expect(result.isProjectComplete).toBe(false);
325
+ const modelsForValidation = vi.mocked(validation_1.validateDbtModel).mock.calls[0][2];
326
+ expect(modelsForValidation).toEqual(expect.arrayContaining([
327
+ expect.objectContaining({
328
+ unique_id: 'model.source_a.orders',
329
+ description: 'Local selected orders',
330
+ lightdash_source_name: 'source_a',
331
+ }),
332
+ expect.objectContaining({
333
+ unique_id: 'model.source_a.customers',
334
+ description: 'Served unselected customers',
335
+ compiled: true,
336
+ lightdash_source_name: 'source_a',
337
+ }),
338
+ expect.objectContaining({
339
+ unique_id: 'model.source_a.deleted',
340
+ lightdash_source_name: 'source_a',
341
+ }),
342
+ expect.objectContaining({
343
+ unique_id: 'model.source_b.customers',
344
+ lightdash_source_name: 'source_b',
345
+ }),
346
+ ]));
347
+ const unselectedCollisionCandidates = modelsForValidation.filter((model) => model.name === 'customers');
348
+ expect(unselectedCollisionCandidates.map((model) => ({
349
+ uniqueId: model.unique_id,
350
+ sourceName: model.lightdash_source_name,
351
+ }))).toEqual([
352
+ {
353
+ uniqueId: 'model.source_a.customers',
354
+ sourceName: 'source_a',
355
+ },
356
+ {
357
+ uniqueId: 'model.source_b.customers',
358
+ sourceName: 'source_b',
359
+ },
360
+ ]);
361
+ });
362
+ test('rejects automatic combination when overlaps identify multiple local sources', async () => {
363
+ const projectManifest = manifest({
364
+ 'model.local.orders': dbtNode('model.local.orders', 'model', true),
365
+ 'model.local.customers': dbtNode('model.local.customers', 'model', true),
366
+ });
367
+ const servedManifest = manifest({
368
+ 'model.local.orders': dbtNode('model.local.orders', 'model', true, {
369
+ lightdash_source_name: 'source_a',
370
+ }),
371
+ 'model.local.customers': dbtNode('model.local.customers', 'model', true, {
372
+ lightdash_source_name: 'source_c',
373
+ }),
374
+ });
375
+ vi.mocked(manifest_1.loadManifest).mockResolvedValue(projectManifest);
376
+ vi.mocked(apiClient_1.lightdashRawApi).mockResolvedValue(new node_fetch_1.Response(JSON.stringify(servedManifest)));
377
+ vi.mocked(compile_2.maybeCompileModelsAndJoins).mockResolvedValue({
378
+ compiledModelIds: ['model.local.orders', 'model.local.customers'],
379
+ originallySelectedModelIds: undefined,
380
+ });
381
+ await expect((0, compile_1.compileProject)({
382
+ ...compileOptions(tempDir),
383
+ combineManifestProjectUuid: 'project-uuid',
384
+ })).rejects.toThrow('Cannot automatically combine manifest from the server: overlapping local models match multiple Lightdash sources (source_a, source_c)');
385
+ expect(validation_1.validateDbtModel).not.toHaveBeenCalled();
386
+ });
387
+ test('skips automatic combination when the local dbt project is not a served source', async () => {
388
+ const projectManifest = manifest({
389
+ 'model.local.orders': dbtNode('model.local.orders', 'model', true),
390
+ });
391
+ const servedManifest = manifest({
392
+ 'model.served.customers': dbtNode('model.served.customers', 'model', true),
393
+ });
394
+ vi.mocked(manifest_1.loadManifest).mockResolvedValue(projectManifest);
395
+ vi.mocked(apiClient_1.lightdashRawApi).mockResolvedValue(new node_fetch_1.Response(JSON.stringify(servedManifest)));
396
+ vi.mocked(compile_2.maybeCompileModelsAndJoins).mockResolvedValue({
397
+ compiledModelIds: ['model.local.orders'],
398
+ originallySelectedModelIds: undefined,
399
+ });
400
+ const result = await (0, compile_1.compileProject)({
401
+ ...compileOptions(tempDir),
402
+ combineManifestProjectUuid: 'project-uuid',
403
+ });
404
+ expect(result.explores.map((explore) => explore.name)).toEqual([
405
+ 'orders',
406
+ ]);
407
+ expect(console.info).toHaveBeenCalledWith(expect.stringContaining('the local dbt project is not a source of this Lightdash project'));
408
+ });
409
+ test('explains when a matching served manifest has no compiled models', async () => {
410
+ const projectManifest = manifest({
411
+ 'model.test.orders': dbtNode('model.test.orders', 'model', true),
412
+ });
413
+ const servedManifest = manifest({
414
+ 'model.test.orders': dbtNode('model.test.orders', 'model', false, {
415
+ lightdash_source_name: 'local_source',
416
+ }),
417
+ 'model.served.helper': dbtNode('model.served.helper', 'model', false),
418
+ });
419
+ vi.mocked(manifest_1.loadManifest).mockResolvedValue(projectManifest);
420
+ vi.mocked(apiClient_1.lightdashRawApi).mockResolvedValue(new node_fetch_1.Response(JSON.stringify(servedManifest)));
421
+ vi.mocked(compile_2.maybeCompileModelsAndJoins).mockResolvedValue({
422
+ compiledModelIds: ['model.test.orders'],
423
+ originallySelectedModelIds: undefined,
424
+ });
425
+ await (0, compile_1.compileProject)({
426
+ ...compileOptions(tempDir),
427
+ combineManifestProjectUuid: 'project-uuid',
428
+ });
429
+ expect(console.info).toHaveBeenCalledWith(expect.stringContaining('contains no compiled models'));
430
+ });
431
+ test('explains when all compiled served models already overlap locally', async () => {
432
+ const projectManifest = manifest({
433
+ 'model.test.orders': dbtNode('model.test.orders', 'model', true),
434
+ });
435
+ const servedManifest = manifest({
436
+ 'model.test.orders': dbtNode('model.test.orders', 'model', true, {
437
+ lightdash_source_name: 'local_source',
438
+ }),
439
+ });
440
+ vi.mocked(manifest_1.loadManifest).mockResolvedValue(projectManifest);
441
+ vi.mocked(apiClient_1.lightdashRawApi).mockResolvedValue(new node_fetch_1.Response(JSON.stringify(servedManifest)));
442
+ vi.mocked(compile_2.maybeCompileModelsAndJoins).mockResolvedValue({
443
+ compiledModelIds: ['model.test.orders'],
444
+ originallySelectedModelIds: undefined,
445
+ });
446
+ await (0, compile_1.compileProject)({
447
+ ...compileOptions(tempDir),
448
+ combineManifestProjectUuid: 'project-uuid',
449
+ });
450
+ expect(console.info).toHaveBeenCalledWith(expect.stringContaining('all compiled models already exist in the preview manifest'));
451
+ });
452
+ test('notices when no served manifest exists and keeps the local manifest', async () => {
453
+ vi.mocked(manifest_1.loadManifest).mockResolvedValue(manifest({
454
+ 'model.test.orders': dbtNode('model.test.orders', 'model', true),
455
+ }));
456
+ vi.mocked(apiClient_1.lightdashRawApi).mockRejectedValue(new common_1.LightdashError({
457
+ message: 'Manifest not found',
458
+ name: 'NotFoundError',
459
+ statusCode: 404,
460
+ data: {},
461
+ }));
462
+ vi.mocked(compile_2.maybeCompileModelsAndJoins).mockResolvedValue({
463
+ compiledModelIds: ['model.test.orders'],
464
+ originallySelectedModelIds: undefined,
465
+ });
466
+ const result = await (0, compile_1.compileProject)({
467
+ ...compileOptions(tempDir),
468
+ combineManifestProjectUuid: 'project-uuid',
469
+ });
470
+ expect(result.explores.map((explore) => explore.name)).toEqual([
471
+ 'orders',
472
+ ]);
473
+ expect(console.error).not.toHaveBeenCalledWith(expect.stringContaining('Could not fetch the server manifest'));
474
+ expect(console.info).toHaveBeenCalledWith(expect.stringContaining('No server manifest found; continuing with the preview manifest'));
475
+ });
476
+ test.each([
477
+ ['a null response', null],
478
+ ['an array of nodes', { metadata: {}, nodes: [] }],
479
+ ['array metadata', { metadata: [], nodes: {} }],
480
+ ['missing metadata', { nodes: {} }],
481
+ ])('rejects %s from the server manifest endpoint and keeps the local manifest', async (_description, invalidManifest) => {
482
+ vi.mocked(manifest_1.loadManifest).mockResolvedValue(manifest({
483
+ 'model.test.orders': dbtNode('model.test.orders', 'model', true),
484
+ }));
485
+ vi.mocked(apiClient_1.lightdashRawApi).mockResolvedValue(new node_fetch_1.Response(JSON.stringify(invalidManifest)));
486
+ vi.mocked(compile_2.maybeCompileModelsAndJoins).mockResolvedValue({
487
+ compiledModelIds: ['model.test.orders'],
488
+ originallySelectedModelIds: undefined,
489
+ });
490
+ const result = await (0, compile_1.compileProject)({
491
+ ...compileOptions(tempDir),
492
+ combineManifestProjectUuid: 'project-uuid',
493
+ });
494
+ expect(result.explores.map((explore) => explore.name)).toEqual([
495
+ 'orders',
496
+ ]);
497
+ expect(console.error).toHaveBeenCalledWith(expect.stringContaining('/api/v1/projects/project-uuid/dbt/manifest returned an invalid manifest'));
498
+ });
499
+ test.each([
500
+ [
501
+ 'an authorization error',
502
+ new common_1.LightdashError({
503
+ message: 'Not authorized',
504
+ name: 'AuthorizationError',
505
+ statusCode: 401,
506
+ data: {},
507
+ }),
508
+ ],
509
+ [
510
+ 'a server error',
511
+ new common_1.LightdashError({
512
+ message: 'Server unavailable',
513
+ name: 'InternalServerError',
514
+ statusCode: 500,
515
+ data: {},
516
+ }),
517
+ ],
518
+ [
519
+ 'a permission error',
520
+ new common_1.LightdashError({
521
+ message: 'Forbidden',
522
+ name: 'ForbiddenError',
523
+ statusCode: 403,
524
+ data: {},
525
+ }),
526
+ ],
527
+ ['a network error', new Error('Connection refused')],
528
+ ])('warns once and keeps the local manifest after %s', async (_description, error) => {
529
+ vi.mocked(manifest_1.loadManifest).mockResolvedValue(manifest({
530
+ 'model.test.orders': dbtNode('model.test.orders', 'model', true),
531
+ }));
532
+ vi.mocked(apiClient_1.lightdashRawApi).mockRejectedValue(error);
533
+ vi.mocked(compile_2.maybeCompileModelsAndJoins).mockResolvedValue({
534
+ compiledModelIds: ['model.test.orders'],
535
+ originallySelectedModelIds: undefined,
536
+ });
537
+ const result = await (0, compile_1.compileProject)({
538
+ ...compileOptions(tempDir),
539
+ combineManifestProjectUuid: 'project-uuid',
540
+ });
541
+ expect(result.explores.map((explore) => explore.name)).toEqual([
542
+ 'orders',
543
+ ]);
544
+ expect(vi
545
+ .mocked(console.error)
546
+ .mock.calls.filter(([message]) => String(message).includes('Could not fetch the server manifest'))).toHaveLength(1);
547
+ });
548
+ test('does not fetch a served manifest when manifest combining is disabled', async () => {
549
+ vi.mocked(manifest_1.loadManifest).mockResolvedValue(manifest({
550
+ 'model.test.orders': dbtNode('model.test.orders', 'model', true),
551
+ }));
552
+ vi.mocked(compile_2.maybeCompileModelsAndJoins).mockResolvedValue({
553
+ compiledModelIds: ['model.test.orders'],
554
+ originallySelectedModelIds: undefined,
555
+ });
556
+ await (0, compile_1.compileProject)({
557
+ ...compileOptions(tempDir),
558
+ combine: false,
559
+ combineManifestProjectUuid: 'project-uuid',
560
+ });
561
+ expect(apiClient_1.lightdashRawApi).not.toHaveBeenCalled();
178
562
  });
179
563
  });
@@ -307,6 +307,7 @@ describe('compile warehouse column validation', () => {
307
307
  projectUuid: 'projectUuid',
308
308
  ignoreErrors: true,
309
309
  validateWarehouseColumns: true,
310
+ batchedDeploy: false,
310
311
  });
311
312
  const deployRequest = vi
312
313
  .mocked(apiClient_1.lightdashApi)
@@ -13,7 +13,7 @@ type DeployHandlerOptions = DbtCompileOptions & {
13
13
  warehouseCredentials?: boolean;
14
14
  organizationCredentials?: string;
15
15
  assumeYes?: boolean;
16
- useBatchedDeploy?: boolean;
16
+ batchedDeploy?: boolean;
17
17
  batchSize?: string;
18
18
  parallelBatches?: string;
19
19
  gzip?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/handlers/deploy.ts"],"names":[],"mappings":"AAAA,OAAO,EAIH,OAAO,EACP,YAAY,EASf,MAAM,mBAAmB,CAAC;AA2B3B,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AASlD,KAAK,oBAAoB,GAAG,iBAAiB,GAAG;IAC5C,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,EAAE,OAAO,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,wBAAwB,EAAE,OAAO,CAAC;IAClC,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAChC,CAAC;AAEF,KAAK,UAAU,GAAG,oBAAoB,GAAG;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AA+QF,eAAO,MAAM,MAAM,aACL,CAAC,OAAO,GAAG,YAAY,CAAC,EAAE,WAC3B,UAAU,KACpB,OAAO,CAAC,IAAI,CA6Id,CAAC;AAiGF,eAAO,MAAM,aAAa,oBAA2B,oBAAoB,kBAuKxE,CAAC"}
1
+ {"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/handlers/deploy.ts"],"names":[],"mappings":"AAAA,OAAO,EAIH,OAAO,EACP,YAAY,EASf,MAAM,mBAAmB,CAAC;AA2B3B,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AASlD,KAAK,oBAAoB,GAAG,iBAAiB,GAAG;IAC5C,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,EAAE,OAAO,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,wBAAwB,EAAE,OAAO,CAAC;IAClC,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAChC,CAAC;AAEF,KAAK,UAAU,GAAG,oBAAoB,GAAG;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AA6RF,eAAO,MAAM,MAAM,aACL,CAAC,OAAO,GAAG,YAAY,CAAC,EAAE,WAC3B,UAAU,KACpB,OAAO,CAAC,IAAI,CA6Id,CAAC;AAiGF,eAAO,MAAM,aAAa,oBAA2B,oBAAoB,kBAuKxE,CAAC"}
@@ -122,13 +122,23 @@ const deployBatched = async (explores, options) => {
122
122
  const deployStartTime = Date.now();
123
123
  // Start deploy session
124
124
  globalState_1.default.log(`Starting deploy session...`);
125
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
126
- const startSessionResponse = (await (0, apiClient_1.lightdashApi)({
127
- method: 'POST',
128
- url: `/api/v2/projects/${options.projectUuid}/deploy`,
129
- body: JSON.stringify({}),
130
- }));
131
- const sessionUuid = startSessionResponse.deploySessionUuid;
125
+ let sessionUuid;
126
+ try {
127
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
128
+ const startSessionResponse = (await (0, apiClient_1.lightdashApi)({
129
+ method: 'POST',
130
+ url: `/api/v2/projects/${options.projectUuid}/deploy`,
131
+ body: JSON.stringify({}),
132
+ }));
133
+ sessionUuid = startSessionResponse.deploySessionUuid;
134
+ }
135
+ catch (error) {
136
+ if (error instanceof common_1.LightdashError && error.statusCode === 404) {
137
+ globalState_1.default.log(styles.info('Batched deploy is not supported by this server; falling back to legacy deploy'));
138
+ return false;
139
+ }
140
+ throw error;
141
+ }
132
142
  globalState_1.default.log(styles.success(`Deploy session created: ${sessionUuid}`));
133
143
  // Split explores into batches
134
144
  const batches = [];
@@ -185,6 +195,7 @@ const deployBatched = async (explores, options) => {
185
195
  durationMs: Date.now() - deployStartTime,
186
196
  },
187
197
  });
198
+ return true;
188
199
  };
189
200
  const deploy = async (explores, options) => {
190
201
  if (explores.length === 0) {
@@ -228,11 +239,9 @@ const deploy = async (explores, options) => {
228
239
  catch (e) {
229
240
  console.error(styles.warning(`\nError replacing project table groups: ${(0, common_1.getErrorMessage)(e)}\n`));
230
241
  }
231
- // Use batched deploy if enabled
232
- if (options.useBatchedDeploy) {
233
- await deployBatched(deployableExplores, options);
234
- }
235
- else {
242
+ const shouldUseLegacyDeploy = options.batchedDeploy === false ||
243
+ !(await deployBatched(deployableExplores, options));
244
+ if (shouldUseLegacyDeploy) {
236
245
  const deployStartTime = Date.now();
237
246
  const deployPayload = JSON.stringify(deployableExplores);
238
247
  try {
@@ -58,7 +58,7 @@ describe('deploy completeness transport', () => {
58
58
  { complete: true, expected: 'true' },
59
59
  { complete: false, expected: 'false' },
60
60
  { complete: undefined, expected: 'false' },
61
- ])('sends complete=$expected on the default upload', async ({ complete, expected }) => {
61
+ ])('sends complete=$expected when batched deploy is disabled', async ({ complete, expected }) => {
62
62
  vi.mocked(apiClient_1.lightdashApi).mockImplementation(async ({ url }) => {
63
63
  if (url.includes('/explores?')) {
64
64
  return {
@@ -74,6 +74,7 @@ describe('deploy completeness transport', () => {
74
74
  await (0, deploy_1.deploy)([compiledExplore], {
75
75
  ...deployOptions,
76
76
  complete,
77
+ batchedDeploy: false,
77
78
  });
78
79
  expect(apiClient_1.lightdashApi).toHaveBeenCalledWith(expect.objectContaining({
79
80
  method: 'PUT',
@@ -81,7 +82,7 @@ describe('deploy completeness transport', () => {
81
82
  body: JSON.stringify([compiledExplore]),
82
83
  }));
83
84
  });
84
- test('includes the completeness assertion in every batch body', async () => {
85
+ test('uses batched deploy by default and includes the completeness assertion in every batch body', async () => {
85
86
  vi.mocked(apiClient_1.lightdashApi).mockImplementation(async ({ url }) => {
86
87
  if (url.endsWith('/deploy')) {
87
88
  return {
@@ -106,7 +107,6 @@ describe('deploy completeness transport', () => {
106
107
  await (0, deploy_1.deploy)([compiledExplore], {
107
108
  ...deployOptions,
108
109
  complete: false,
109
- useBatchedDeploy: true,
110
110
  });
111
111
  expect(apiClient_1.lightdashApi).toHaveBeenCalledWith({
112
112
  method: 'POST',
@@ -118,4 +118,51 @@ describe('deploy completeness transport', () => {
118
118
  }),
119
119
  });
120
120
  });
121
+ test('falls back to the legacy deploy when session creation returns 404', async () => {
122
+ vi.mocked(apiClient_1.lightdashApi).mockImplementation(async ({ url }) => {
123
+ if (url.endsWith('/deploy')) {
124
+ throw new common_1.LightdashError({
125
+ message: 'Not found',
126
+ name: 'NotFoundError',
127
+ statusCode: 404,
128
+ data: {},
129
+ });
130
+ }
131
+ if (url.includes('/explores?')) {
132
+ return {
133
+ exploreCount: 1,
134
+ warnings: {
135
+ warningCount: 0,
136
+ exploresWithWarnings: [],
137
+ },
138
+ };
139
+ }
140
+ return null;
141
+ });
142
+ await (0, deploy_1.deploy)([compiledExplore], deployOptions);
143
+ expect(apiClient_1.lightdashApi).toHaveBeenCalledWith({
144
+ method: 'PUT',
145
+ url: '/api/v1/projects/project-uuid/explores?complete=false',
146
+ body: JSON.stringify([compiledExplore]),
147
+ });
148
+ });
149
+ test('propagates non-404 session creation errors', async () => {
150
+ const serverError = new common_1.LightdashError({
151
+ message: 'Server unavailable',
152
+ name: 'InternalServerError',
153
+ statusCode: 500,
154
+ data: {},
155
+ });
156
+ vi.mocked(apiClient_1.lightdashApi).mockImplementation(async ({ url }) => {
157
+ if (url.endsWith('/deploy')) {
158
+ throw serverError;
159
+ }
160
+ return null;
161
+ });
162
+ await expect((0, deploy_1.deploy)([compiledExplore], deployOptions)).rejects.toBe(serverError);
163
+ expect(apiClient_1.lightdashApi).not.toHaveBeenCalledWith(expect.objectContaining({
164
+ method: 'PUT',
165
+ url: expect.stringContaining('/explores?'),
166
+ }));
167
+ });
121
168
  });
@@ -14,13 +14,15 @@ type PreviewHandlerOptions = DbtCompileOptions & {
14
14
  organizationCredentials?: string;
15
15
  assumeYes?: boolean;
16
16
  warehouseCredentials?: boolean;
17
- useBatchedDeploy?: boolean;
17
+ batchedDeploy?: boolean;
18
18
  batchSize?: string;
19
19
  parallelBatches?: string;
20
20
  expiresIn?: string;
21
21
  disableTimestampConversion?: boolean;
22
22
  validateWarehouseColumns: boolean;
23
23
  partialCompilation?: boolean;
24
+ combine?: boolean;
25
+ combineManifestProjectUuid?: string;
24
26
  };
25
27
  type StopPreviewHandlerOptions = {
26
28
  name: string;
@@ -1 +1 @@
1
- {"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../../src/handlers/preview.ts"],"names":[],"mappings":"AACA,OAAO,EACH,+BAA+B,EAKlC,MAAM,mBAAmB,CAAC;AAgB3B,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAOlD,KAAK,qBAAqB,GAAG,iBAAiB,GAAG;IAC7C,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,OAAO,CAAC;IACtB,kBAAkB,EAAE,+BAA+B,CAAC;IACpD,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,wBAAwB,EAAE,OAAO,CAAC;IAClC,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAChC,CAAC;AAEF,KAAK,yBAAyB,GAAG;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;CACpB,CAAC;AA2GF,eAAO,MAAM,cAAc,oBACN,qBAAqB,KACvC,OAAO,CAAC,IAAI,CAsSd,CAAC;AAEF,eAAO,MAAM,mBAAmB,oBACX,qBAAqB,KACvC,OAAO,CAAC,IAAI,CAqLd,CAAC;AAEF,eAAO,MAAM,kBAAkB,YAClB,yBAAyB,KACnC,OAAO,CAAC,IAAI,CA0Cd,CAAC"}
1
+ {"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../../src/handlers/preview.ts"],"names":[],"mappings":"AACA,OAAO,EACH,+BAA+B,EAKlC,MAAM,mBAAmB,CAAC;AAgB3B,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAOlD,KAAK,qBAAqB,GAAG,iBAAiB,GAAG;IAC7C,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,OAAO,CAAC;IACtB,kBAAkB,EAAE,+BAA+B,CAAC;IACpD,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,wBAAwB,EAAE,OAAO,CAAC;IAClC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,0BAA0B,CAAC,EAAE,MAAM,CAAC;CACvC,CAAC;AAEF,KAAK,yBAAyB,GAAG;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;CACpB,CAAC;AA2GF,eAAO,MAAM,cAAc,oBACN,qBAAqB,KACvC,OAAO,CAAC,IAAI,CAuSd,CAAC;AAEF,eAAO,MAAM,mBAAmB,oBACX,qBAAqB,KACvC,OAAO,CAAC,IAAI,CAsLd,CAAC;AAEF,eAAO,MAAM,kBAAkB,YAClB,yBAAyB,KACnC,OAAO,CAAC,IAAI,CA0Cd,CAAC"}
@@ -132,6 +132,7 @@ const previewHandler = async (originalOptions) => {
132
132
  let contentCopyError;
133
133
  let contentCopySkipReason;
134
134
  const config = await (0, config_1.getConfig)();
135
+ options.combineManifestProjectUuid = config.context?.project;
135
136
  // Validate upstream project before attempting to copy permissions or content
136
137
  let upstreamProjectValid = false;
137
138
  if (config.context?.project) {
@@ -328,6 +329,7 @@ const startPreviewHandler = async (originalOptions) => {
328
329
  });
329
330
  const projectName = options.name;
330
331
  const config = await (0, config_1.getConfig)();
332
+ options.combineManifestProjectUuid = config.context?.project;
331
333
  // Log current source project info if copying content
332
334
  if (!options.skipCopyContent && config.context?.project) {
333
335
  console.error(`\n${styles.success('Source project for content:')} ${styles.bold(config.context.projectName || config.context.project)}\n`);