@lightdash/cli 2.185.3 → 2.193.1

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.
@@ -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,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"}
1
+ {"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../../src/handlers/preview.ts"],"names":[],"mappings":"AACA,OAAO,EACH,+BAA+B,EAKlC,MAAM,mBAAmB,CAAC;AAmB3B,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAYlD,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;AAuDF,eAAO,MAAM,mBAAmB,oBACX,qBAAqB,KACvC,OAAO,CAAC,IAAI,CA2Ld,CAAC;AAEF,eAAO,MAAM,kBAAkB,YAClB,yBAAyB,KACnC,OAAO,CAAC,IAAI,CA0Cd,CAAC"}
@@ -19,8 +19,10 @@ const styles = tslib_1.__importStar(require("../styles"));
19
19
  const compile_1 = require("./compile");
20
20
  const createProject_1 = require("./createProject");
21
21
  const apiClient_1 = require("./dbt/apiClient");
22
+ const refresh_1 = require("./dbt/refresh");
22
23
  const deploy_1 = require("./deploy");
23
24
  const timestampConversion_1 = require("./timestampConversion");
25
+ const warehouseConnection_1 = require("./warehouseConnection");
24
26
  const deletePreviewProject = async (projectUuid) => {
25
27
  /**
26
28
  * projectUuid may be undefined here if a command fails early enough
@@ -309,6 +311,42 @@ const previewHandler = async (originalOptions) => {
309
311
  await cleanupProject(executionId, project.projectUuid, previewStartTime);
310
312
  };
311
313
  exports.previewHandler = previewHandler;
314
+ // Credentials resolved locally (AWS SSO, Snowflake SSO) expire, so a re-run
315
+ // with the same name pushes freshly resolved ones into the existing preview.
316
+ const refreshPreviewWarehouseCredentials = async (projectUuid, options) => {
317
+ const source = (0, warehouseConnection_1.getWarehouseCredentialsSource)(options);
318
+ if (source.source === 'organization') {
319
+ globalState_1.default.debug(`> Preview uses organization warehouse credentials "${source.name}", nothing to refresh`);
320
+ return;
321
+ }
322
+ if (source.source === 'none') {
323
+ globalState_1.default.debug('> Skipping warehouse credentials refresh (--no-warehouse-credentials)');
324
+ return;
325
+ }
326
+ const loaded = await (0, createProject_1.loadWarehouseCredentialsFromProfiles)({
327
+ projectDir: options.projectDir,
328
+ profilesDir: options.profilesDir,
329
+ target: options.target,
330
+ profile: options.profile,
331
+ startOfWeek: options.startOfWeek,
332
+ assumeYes: options.assumeYes,
333
+ targetPath: options.targetPath,
334
+ });
335
+ if (!loaded) {
336
+ console.error(styles.warning('Keeping the warehouse credentials already stored on the preview.'));
337
+ return;
338
+ }
339
+ const spinner = globalState_1.default.startSpinner(' Refreshing warehouse credentials...');
340
+ try {
341
+ const project = await (0, refresh_1.getProject)(projectUuid);
342
+ await (0, warehouseConnection_1.updateProjectWarehouseConnection)(project, loaded.credentials, 'Refreshing warehouse credentials');
343
+ spinner.succeed(' Warehouse credentials refreshed');
344
+ }
345
+ catch (e) {
346
+ spinner.fail();
347
+ throw e;
348
+ }
349
+ };
312
350
  const startPreviewHandler = async (originalOptions) => {
313
351
  const options = { ...originalOptions };
314
352
  globalState_1.default.setVerbose(options.verbose);
@@ -350,6 +388,10 @@ const startPreviewHandler = async (originalOptions) => {
350
388
  // so refresh its expiration (uses the upstream default when
351
389
  // --expires-in is not set)
352
390
  await updatePreviewExpiration(previewProject.projectUuid, options.expiresIn);
391
+ await refreshPreviewWarehouseCredentials(previewProject.projectUuid, {
392
+ ...options,
393
+ warehouseCredentials: projectTypeConfig.warehouseCredentials,
394
+ });
353
395
  // Update
354
396
  options.disableTimestampConversion =
355
397
  await (0, timestampConversion_1.getProjectDisableTimestampConversion)(options.disableTimestampConversion, previewProject.projectUuid);
@@ -1 +1 @@
1
- {"version":3,"file":"setProject.d.ts","sourceRoot":"","sources":["../../src/handlers/setProject.ts"],"names":[],"mappings":"AAQA,KAAK,iBAAiB,GAAG;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,eAAO,MAAM,iBAAiB,UACnB,MAAM,eACA,MAAM,KACpB,OAAO,CAAC,UAAU,GAAG,SAAS,GAAG,OAAO,CAoF1C,CAAC;AAEF,eAAO,MAAM,eAAe,qBAmB3B,CAAC;AAEF,eAAO,MAAM,iBAAiB,YAAmB,iBAAiB,kBAsBjE,CAAC;AAOF,eAAO,MAAM,mBAAmB,YAAmB;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,kBAmBtE,CAAC"}
1
+ {"version":3,"file":"setProject.d.ts","sourceRoot":"","sources":["../../src/handlers/setProject.ts"],"names":[],"mappings":"AAQA,KAAK,iBAAiB,GAAG;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,eAAO,MAAM,iBAAiB,UACnB,MAAM,eACA,MAAM,KACpB,OAAO,CAAC,UAAU,GAAG,SAAS,GAAG,OAAO,CAiF1C,CAAC;AAEF,eAAO,MAAM,eAAe,qBAmB3B,CAAC;AAEF,eAAO,MAAM,iBAAiB,YAAmB,iBAAiB,kBAsBjE,CAAC;AAOF,eAAO,MAAM,mBAAmB,YAAmB;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,kBAmBtE,CAAC"}
@@ -3,11 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.unsetProjectHandler = exports.setProjectHandler = exports.setFirstProject = exports.setProjectCommand = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const common_1 = require("@lightdash/common");
6
- const inquirer_1 = tslib_1.__importDefault(require("inquirer"));
7
6
  const url_1 = require("url");
8
7
  const analytics_1 = require("../analytics/analytics");
9
8
  const config_1 = require("../config");
10
9
  const globalState_1 = tslib_1.__importDefault(require("../globalState"));
10
+ const fuzzySearchPrompt_1 = require("../prompts/fuzzySearchPrompt");
11
11
  const apiClient_1 = require("./dbt/apiClient");
12
12
  const setProjectCommand = async (name, uuidOrSlug) => {
13
13
  const projects = await (0, apiClient_1.lightdashApi)({
@@ -40,27 +40,24 @@ const setProjectCommand = async (name, uuidOrSlug) => {
40
40
  }
41
41
  else {
42
42
  const SKIP_VALUE = '__skip__';
43
- const answers = await inquirer_1.default.prompt([
44
- {
45
- type: 'list',
46
- name: 'project',
47
- choices: [
48
- {
49
- name: "Don't select a project",
50
- value: SKIP_VALUE,
51
- },
52
- ...nonPreviewProjects.map((project) => ({
53
- name: project.name,
54
- value: project.projectUuid,
55
- })),
56
- ],
57
- },
58
- ]);
59
- if (answers.project === SKIP_VALUE) {
43
+ const answer = await (0, fuzzySearchPrompt_1.promptFuzzySearch)({
44
+ message: 'Select a project (type to search)',
45
+ pinnedChoices: [
46
+ {
47
+ name: "Don't select a project",
48
+ value: SKIP_VALUE,
49
+ },
50
+ ],
51
+ choices: nonPreviewProjects.map((project) => ({
52
+ name: project.name,
53
+ value: project.projectUuid,
54
+ })),
55
+ });
56
+ if (answer === SKIP_VALUE) {
60
57
  await (0, config_1.unsetProject)();
61
58
  return 'skipped';
62
59
  }
63
- selectedProject = nonPreviewProjects.find((project) => project.projectUuid === answers.project);
60
+ selectedProject = nonPreviewProjects.find((project) => project.projectUuid === answer);
64
61
  }
65
62
  if (selectedProject !== undefined) {
66
63
  await (0, config_1.setProject)(selectedProject.projectUuid, selectedProject.name);
@@ -2,12 +2,14 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const tslib_1 = require("tslib");
4
4
  const common_1 = require("@lightdash/common");
5
- const inquirer_1 = tslib_1.__importDefault(require("inquirer"));
6
5
  const config_1 = require("../config");
7
6
  const globalState_1 = tslib_1.__importDefault(require("../globalState"));
7
+ const fuzzySearchPrompt_1 = require("../prompts/fuzzySearchPrompt");
8
8
  const apiClient_1 = require("./dbt/apiClient");
9
9
  const setProject_1 = require("./setProject");
10
- vi.mock('inquirer');
10
+ vi.mock('../prompts/fuzzySearchPrompt', () => ({
11
+ promptFuzzySearch: vi.fn(),
12
+ }));
11
13
  vi.mock('../analytics/analytics');
12
14
  vi.mock('./dbt/apiClient', () => ({
13
15
  lightdashApi: vi.fn(),
@@ -18,7 +20,7 @@ vi.mock('../config', () => ({
18
20
  unsetProject: vi.fn().mockResolvedValue(undefined),
19
21
  }));
20
22
  const mockLightdashApi = apiClient_1.lightdashApi;
21
- const mockInquirer = inquirer_1.default;
23
+ const mockPromptFuzzySearch = fuzzySearchPrompt_1.promptFuzzySearch;
22
24
  const mockSetProjectConfig = config_1.setProject;
23
25
  const MAIN_UUID = '00000000-0000-0000-0000-000000000001';
24
26
  const PREVIEW_UUID = '00000000-0000-0000-0000-000000000002';
@@ -45,13 +47,10 @@ describe('setProjectCommand', () => {
45
47
  });
46
48
  it('does not offer preview projects in the interactive list', async () => {
47
49
  mockLightdashApi.mockResolvedValueOnce(buildProjects());
48
- const promptMock = vi
49
- .fn()
50
- .mockResolvedValueOnce({ project: MAIN_UUID });
51
- mockInquirer.prompt = promptMock;
50
+ mockPromptFuzzySearch.mockResolvedValueOnce(MAIN_UUID);
52
51
  await (0, setProject_1.setProjectCommand)();
53
- const promptArgs = promptMock.mock.calls[0][0];
54
- const choices = promptArgs[0].choices.map((c) => c.value);
52
+ const promptArgs = mockPromptFuzzySearch.mock.calls[0][0];
53
+ const choices = promptArgs.choices.map((c) => c.value);
55
54
  expect(choices).toContain(MAIN_UUID);
56
55
  expect(choices).not.toContain(PREVIEW_UUID);
57
56
  expect(mockSetProjectConfig).toHaveBeenCalledWith(MAIN_UUID, 'main-project');
@@ -1 +1 @@
1
- {"version":3,"file":"setWarehouse.d.ts","sourceRoot":"","sources":["../../src/handlers/setWarehouse.ts"],"names":[],"mappings":"AAcA,KAAK,0BAA0B,GAAG;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;CACpB,CAAC;AAeF,eAAO,MAAM,mBAAmB,YACnB,0BAA0B,kBAyFtC,CAAC"}
1
+ {"version":3,"file":"setWarehouse.d.ts","sourceRoot":"","sources":["../../src/handlers/setWarehouse.ts"],"names":[],"mappings":"AAWA,KAAK,0BAA0B,GAAG;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;CACpB,CAAC;AAeF,eAAO,MAAM,mBAAmB,YACnB,0BAA0B,kBA0EtC,CAAC"}
@@ -11,6 +11,7 @@ const createProject_1 = require("./createProject");
11
11
  const apiClient_1 = require("./dbt/apiClient");
12
12
  const refresh_1 = require("./dbt/refresh");
13
13
  const resolveProjectFlag_1 = require("./resolveProjectFlag");
14
+ const warehouseConnection_1 = require("./warehouseConnection");
14
15
  const resolveProjectUuid = async (projectOption) => {
15
16
  if (projectOption) {
16
17
  return (0, resolveProjectFlag_1.resolveProjectFlag)(projectOption);
@@ -62,23 +63,7 @@ const setWarehouseHandler = async (options) => {
62
63
  }
63
64
  const spinner = globalState_1.default.startSpinner(' Updating warehouse connection...');
64
65
  try {
65
- // Build UpdateProject body — preserve existing fields, override warehouseConnection.
66
- // Note: dbtConnection from GET response may have stripped secrets, but the backend's
67
- // mergeMissingProjectConfigSecrets fills them back in from the saved project before persisting.
68
- const updateBody = {
69
- name: existingProject.name,
70
- dbtConnection: existingProject.dbtConnection,
71
- dbtVersion: existingProject.dbtVersion,
72
- warehouseConnection: credentials,
73
- };
74
- // PATCH project — triggers adaptor test + recompile
75
- const result = await (0, apiClient_1.lightdashApi)({
76
- method: 'PATCH',
77
- url: `/api/v1/projects/${projectUuid}`,
78
- body: JSON.stringify(updateBody),
79
- });
80
- // Poll until job completes (custom spinner prefix)
81
- await (0, refresh_1.getFinalJobState)(result.jobUuid, 'Updating warehouse connection');
66
+ await (0, warehouseConnection_1.updateProjectWarehouseConnection)(existingProject, credentials, 'Updating warehouse connection');
82
67
  spinner.stop();
83
68
  }
84
69
  catch (e) {
@@ -0,0 +1,16 @@
1
+ import { type CreateWarehouseCredentials, type Project } from '@lightdash/common';
2
+ type WarehouseCredentialsSource = {
3
+ source: 'profiles';
4
+ } | {
5
+ source: 'organization';
6
+ name: string;
7
+ } | {
8
+ source: 'none';
9
+ };
10
+ export declare const getWarehouseCredentialsSource: (options: {
11
+ organizationCredentials?: string;
12
+ warehouseCredentials?: boolean;
13
+ }) => WarehouseCredentialsSource;
14
+ export declare const updateProjectWarehouseConnection: (project: Pick<Project, 'projectUuid' | 'name' | 'dbtConnection' | 'dbtVersion'>, credentials: CreateWarehouseCredentials, jobLabel: string) => Promise<void>;
15
+ export {};
16
+ //# sourceMappingURL=warehouseConnection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"warehouseConnection.d.ts","sourceRoot":"","sources":["../../src/handlers/warehouseConnection.ts"],"names":[],"mappings":"AAAA,OAAO,EAEH,KAAK,0BAA0B,EAC/B,KAAK,OAAO,EAEf,MAAM,mBAAmB,CAAC;AAI3B,KAAK,0BAA0B,GACzB;IAAE,MAAM,EAAE,UAAU,CAAA;CAAE,GACtB;IAAE,MAAM,EAAE,cAAc,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACxC;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzB,eAAO,MAAM,6BAA6B,YAAa;IACnD,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAClC,KAAG,0BAWH,CAAC;AAEF,eAAO,MAAM,gCAAgC,YAChC,IAAI,CACT,OAAO,EACP,aAAa,GAAG,MAAM,GAAG,eAAe,GAAG,YAAY,CAC1D,eACY,0BAA0B,YAC7B,MAAM,KACjB,OAAO,CAAC,IAAI,CAmBd,CAAC"}
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.updateProjectWarehouseConnection = exports.getWarehouseCredentialsSource = void 0;
4
+ const apiClient_1 = require("./dbt/apiClient");
5
+ const refresh_1 = require("./dbt/refresh");
6
+ const getWarehouseCredentialsSource = (options) => {
7
+ if (options.organizationCredentials) {
8
+ return {
9
+ source: 'organization',
10
+ name: options.organizationCredentials,
11
+ };
12
+ }
13
+ if (options.warehouseCredentials === false) {
14
+ return { source: 'none' };
15
+ }
16
+ return { source: 'profiles' };
17
+ };
18
+ exports.getWarehouseCredentialsSource = getWarehouseCredentialsSource;
19
+ const updateProjectWarehouseConnection = async (project, credentials, jobLabel) => {
20
+ // Build UpdateProject body — preserve existing fields, override warehouseConnection.
21
+ // Note: dbtConnection from GET response may have stripped secrets, but the backend's
22
+ // mergeMissingProjectConfigSecrets fills them back in from the saved project before persisting.
23
+ const updateBody = {
24
+ name: project.name,
25
+ dbtConnection: project.dbtConnection,
26
+ dbtVersion: project.dbtVersion,
27
+ warehouseConnection: credentials,
28
+ };
29
+ // PATCH project — triggers adaptor test + recompile. CLI-deployed projects
30
+ // (dbtConnection type "none") only run the adaptor test and keep their explores.
31
+ const result = await (0, apiClient_1.lightdashApi)({
32
+ method: 'PATCH',
33
+ url: `/api/v1/projects/${project.projectUuid}`,
34
+ body: JSON.stringify(updateBody),
35
+ });
36
+ await (0, refresh_1.getFinalJobState)(result.jobUuid, jobLabel);
37
+ };
38
+ exports.updateProjectWarehouseConnection = updateProjectWarehouseConnection;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=warehouseConnection.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"warehouseConnection.test.d.ts","sourceRoot":"","sources":["../../src/handlers/warehouseConnection.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const common_1 = require("@lightdash/common");
4
+ const apiClient_1 = require("./dbt/apiClient");
5
+ const refresh_1 = require("./dbt/refresh");
6
+ const warehouseConnection_1 = require("./warehouseConnection");
7
+ vi.mock('./dbt/apiClient', () => ({
8
+ lightdashApi: vi.fn(),
9
+ }));
10
+ vi.mock('./dbt/refresh', () => ({
11
+ getFinalJobState: vi.fn(),
12
+ }));
13
+ const mockLightdashApi = apiClient_1.lightdashApi;
14
+ const mockGetFinalJobState = refresh_1.getFinalJobState;
15
+ const PROJECT_UUID = '00000000-0000-0000-0000-000000000001';
16
+ const project = {
17
+ projectUuid: PROJECT_UUID,
18
+ name: 'my-preview',
19
+ dbtConnection: { type: common_1.DbtProjectType.NONE },
20
+ dbtVersion: common_1.SupportedDbtVersions.V1_10,
21
+ };
22
+ const credentials = {
23
+ type: common_1.WarehouseTypes.POSTGRES,
24
+ host: 'localhost',
25
+ user: 'refreshed-user',
26
+ password: 'refreshed-password',
27
+ port: 5432,
28
+ dbname: 'db',
29
+ schema: 'public',
30
+ };
31
+ describe('getWarehouseCredentialsSource', () => {
32
+ it('loads from profiles by default', () => {
33
+ expect((0, warehouseConnection_1.getWarehouseCredentialsSource)({})).toEqual({
34
+ source: 'profiles',
35
+ });
36
+ expect((0, warehouseConnection_1.getWarehouseCredentialsSource)({ warehouseCredentials: true })).toEqual({ source: 'profiles' });
37
+ });
38
+ it('skips when --no-warehouse-credentials is set', () => {
39
+ expect((0, warehouseConnection_1.getWarehouseCredentialsSource)({ warehouseCredentials: false })).toEqual({ source: 'none' });
40
+ });
41
+ it('prefers organization credentials over profiles', () => {
42
+ expect((0, warehouseConnection_1.getWarehouseCredentialsSource)({
43
+ organizationCredentials: 'shared-redshift',
44
+ warehouseCredentials: false,
45
+ })).toEqual({ source: 'organization', name: 'shared-redshift' });
46
+ });
47
+ });
48
+ describe('updateProjectWarehouseConnection', () => {
49
+ beforeEach(() => {
50
+ vi.clearAllMocks();
51
+ });
52
+ it('patches the project with the new credentials and waits for the job', async () => {
53
+ mockLightdashApi.mockResolvedValueOnce({ jobUuid: 'job-1' });
54
+ mockGetFinalJobState.mockResolvedValueOnce({
55
+ jobStatus: common_1.JobStatusType.DONE,
56
+ });
57
+ await (0, warehouseConnection_1.updateProjectWarehouseConnection)(project, credentials, 'Refreshing warehouse credentials');
58
+ expect(mockLightdashApi).toHaveBeenCalledTimes(1);
59
+ const call = mockLightdashApi.mock.calls[0][0];
60
+ expect(call.method).toBe('PATCH');
61
+ expect(call.url).toBe(`/api/v1/projects/${PROJECT_UUID}`);
62
+ expect(JSON.parse(call.body)).toEqual({
63
+ name: 'my-preview',
64
+ dbtConnection: { type: common_1.DbtProjectType.NONE },
65
+ dbtVersion: common_1.SupportedDbtVersions.V1_10,
66
+ warehouseConnection: credentials,
67
+ });
68
+ expect(mockGetFinalJobState).toHaveBeenCalledWith('job-1', 'Refreshing warehouse credentials');
69
+ });
70
+ it('surfaces a failed adapter test', async () => {
71
+ mockLightdashApi.mockResolvedValueOnce({ jobUuid: 'job-2' });
72
+ mockGetFinalJobState.mockRejectedValueOnce(new Error('Your Redshift IAM AWS session has expired'));
73
+ await expect((0, warehouseConnection_1.updateProjectWarehouseConnection)(project, credentials, 'label')).rejects.toThrow('session has expired');
74
+ });
75
+ });
package/dist/index.js CHANGED
@@ -316,7 +316,7 @@ commander_1.program
316
316
  .option('--table-configuration <prod|all>', `If set to 'prod' it will copy the table configuration from prod project`, 'all')
317
317
  .option('--skip-copy-content', 'Skip copying content from the source project', false)
318
318
  .option('--disable-timestamp-conversion [true|false]', 'Disable timestamp conversion to UTC for Snowflake warehouses. Applied when creating the preview project; ignored when updating an existing preview (read from project settings).', parseDisableTimestampConversionOption)
319
- .option('--no-warehouse-credentials', 'Create preview without warehouse credentials. Copies credentials from upstream project.')
319
+ .option('--no-warehouse-credentials', 'Create preview without warehouse credentials (copied from the upstream project). When updating an existing preview, keep the credentials it already has.')
320
320
  .option('-y, --assume-yes', 'assume yes to prompts', false)
321
321
  .option('--no-batched-deploy', 'Use the legacy single-request deploy')
322
322
  .option('--use-batched-deploy', 'Use batched deploy to upload explores in batches')
@@ -0,0 +1,19 @@
1
+ export type FuzzyChoice<T> = {
2
+ name: string;
3
+ value: T;
4
+ };
5
+ /**
6
+ * Filters and ranks choices by a search term. Every whitespace-separated token
7
+ * must match, so "prod eu" narrows to names containing both.
8
+ */
9
+ export declare const fuzzyFilterChoices: <T>(choices: FuzzyChoice<T>[], search: string) => FuzzyChoice<T>[];
10
+ /**
11
+ * A list prompt you can type into to narrow down the options.
12
+ * `pinnedChoices` are shown at the top while nothing has been typed.
13
+ */
14
+ export declare const promptFuzzySearch: <T>({ message, choices, pinnedChoices, }: {
15
+ message: string;
16
+ choices: FuzzyChoice<T>[];
17
+ pinnedChoices?: FuzzyChoice<T>[];
18
+ }) => Promise<T>;
19
+ //# sourceMappingURL=fuzzySearchPrompt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fuzzySearchPrompt.d.ts","sourceRoot":"","sources":["../../src/prompts/fuzzySearchPrompt.ts"],"names":[],"mappings":"AAYA,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,CAAC,CAAC;CACZ,CAAC;AAmCF;;;GAGG;AACH,eAAO,MAAM,kBAAkB,GAAI,CAAC,WACvB,WAAW,CAAC,CAAC,CAAC,EAAE,UACjB,MAAM,KACf,WAAW,CAAC,CAAC,CAAC,EAoBhB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,iBAAiB,GAAU,CAAC,wCAItC;IACC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC;IAC1B,aAAa,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC;CACpC,KAAG,OAAO,CAAC,CAAC,CA+BZ,CAAC"}
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.promptFuzzySearch = exports.fuzzyFilterChoices = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const inquirer_1 = tslib_1.__importDefault(require("inquirer"));
6
+ const inquirer_autocomplete_prompt_1 = tslib_1.__importDefault(require("inquirer-autocomplete-prompt"));
7
+ const PROMPT_TYPE = 'autocomplete';
8
+ let registered = false;
9
+ const registerPrompt = () => {
10
+ if (registered)
11
+ return;
12
+ inquirer_1.default.registerPrompt(PROMPT_TYPE, inquirer_autocomplete_prompt_1.default);
13
+ registered = true;
14
+ };
15
+ /**
16
+ * Scores a choice against a search term. Returns null when it doesn't match.
17
+ * Lower scores rank higher: exact prefix beats substring beats subsequence.
18
+ */
19
+ const scoreChoice = (name, search) => {
20
+ const haystack = name.toLowerCase();
21
+ const needle = search.toLowerCase();
22
+ if (needle.length === 0)
23
+ return 0;
24
+ const index = haystack.indexOf(needle);
25
+ if (index === 0)
26
+ return 0;
27
+ if (index > 0)
28
+ return 1000 + index;
29
+ // Subsequence match: characters appear in order but not contiguously.
30
+ let cursor = 0;
31
+ let firstMatch = -1;
32
+ let gaps = 0;
33
+ for (const char of needle) {
34
+ const found = haystack.indexOf(char, cursor);
35
+ if (found === -1)
36
+ return null;
37
+ if (firstMatch === -1)
38
+ firstMatch = found;
39
+ else
40
+ gaps += found - cursor;
41
+ cursor = found + 1;
42
+ }
43
+ return 100000 + gaps + firstMatch;
44
+ };
45
+ /**
46
+ * Filters and ranks choices by a search term. Every whitespace-separated token
47
+ * must match, so "prod eu" narrows to names containing both.
48
+ */
49
+ const fuzzyFilterChoices = (choices, search) => {
50
+ const tokens = search.trim().split(/\s+/).filter(Boolean);
51
+ if (tokens.length === 0)
52
+ return choices;
53
+ return (choices
54
+ .reduce((acc, choice) => {
55
+ let total = 0;
56
+ for (const token of tokens) {
57
+ const score = scoreChoice(choice.name, token);
58
+ if (score === null)
59
+ return acc;
60
+ total += score;
61
+ }
62
+ acc.push({ choice, score: total });
63
+ return acc;
64
+ }, [])
65
+ // Array#sort is stable, so equally-scored choices keep their order.
66
+ .sort((a, b) => a.score - b.score)
67
+ .map(({ choice }) => choice));
68
+ };
69
+ exports.fuzzyFilterChoices = fuzzyFilterChoices;
70
+ /**
71
+ * A list prompt you can type into to narrow down the options.
72
+ * `pinnedChoices` are shown at the top while nothing has been typed.
73
+ */
74
+ const promptFuzzySearch = async ({ message, choices, pinnedChoices = [], }) => {
75
+ const allChoices = [...pinnedChoices, ...choices];
76
+ // Searching needs a terminal to type into: without one the autocomplete
77
+ // prompt gets the whole piped line at once and never resolves a choice.
78
+ if (!process.stdin.isTTY) {
79
+ const { answer } = await inquirer_1.default.prompt([
80
+ { type: 'list', name: 'answer', message, choices: allChoices },
81
+ ]);
82
+ return answer;
83
+ }
84
+ registerPrompt();
85
+ const { answer } = await inquirer_1.default.prompt([
86
+ {
87
+ type: PROMPT_TYPE,
88
+ name: 'answer',
89
+ message,
90
+ emptyText: 'No matches',
91
+ // inquirer-autocomplete-prompt passes `undefined` on first render.
92
+ source: (_answersSoFar, search) => {
93
+ const term = search ?? '';
94
+ return Promise.resolve(term.trim() === ''
95
+ ? allChoices
96
+ : (0, exports.fuzzyFilterChoices)(choices, term));
97
+ },
98
+ },
99
+ ]);
100
+ return answer;
101
+ };
102
+ exports.promptFuzzySearch = promptFuzzySearch;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=fuzzySearchPrompt.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fuzzySearchPrompt.test.d.ts","sourceRoot":"","sources":["../../src/prompts/fuzzySearchPrompt.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const fuzzySearchPrompt_1 = require("./fuzzySearchPrompt");
4
+ const buildChoices = (names) => names.map((name) => ({ name, value: name }));
5
+ describe('fuzzyFilterChoices', () => {
6
+ it('returns every choice when the search is empty', () => {
7
+ const choices = buildChoices(['alpha', 'beta']);
8
+ expect((0, fuzzySearchPrompt_1.fuzzyFilterChoices)(choices, '')).toEqual(choices);
9
+ expect((0, fuzzySearchPrompt_1.fuzzyFilterChoices)(choices, ' ')).toEqual(choices);
10
+ });
11
+ it('matches case-insensitively on substrings', () => {
12
+ const choices = buildChoices(['Jaffle Shop', 'Internal analytics']);
13
+ expect((0, fuzzySearchPrompt_1.fuzzyFilterChoices)(choices, 'analytics').map((c) => c.name)).toEqual(['Internal analytics']);
14
+ });
15
+ it('matches out-of-order tokens', () => {
16
+ const choices = buildChoices([
17
+ 'production-eu',
18
+ 'production-us',
19
+ 'staging-eu',
20
+ ]);
21
+ expect((0, fuzzySearchPrompt_1.fuzzyFilterChoices)(choices, 'eu prod').map((c) => c.name)).toEqual(['production-eu']);
22
+ });
23
+ it('matches subsequences', () => {
24
+ const choices = buildChoices(['production-europe', 'staging']);
25
+ expect((0, fuzzySearchPrompt_1.fuzzyFilterChoices)(choices, 'pdeu').map((c) => c.name)).toEqual([
26
+ 'production-europe',
27
+ ]);
28
+ });
29
+ it('ranks prefix matches above substring and subsequence matches', () => {
30
+ const choices = buildChoices([
31
+ 'my-prod-clone',
32
+ 'production',
33
+ 'p-r-o-d',
34
+ ]);
35
+ expect((0, fuzzySearchPrompt_1.fuzzyFilterChoices)(choices, 'prod').map((c) => c.name)).toEqual([
36
+ 'production',
37
+ 'my-prod-clone',
38
+ 'p-r-o-d',
39
+ ]);
40
+ });
41
+ it('keeps the original order between equally-ranked choices', () => {
42
+ const choices = buildChoices(['prod-a', 'prod-b', 'prod-c']);
43
+ expect((0, fuzzySearchPrompt_1.fuzzyFilterChoices)(choices, 'prod').map((c) => c.name)).toEqual([
44
+ 'prod-a',
45
+ 'prod-b',
46
+ 'prod-c',
47
+ ]);
48
+ });
49
+ it('returns nothing when a token does not match', () => {
50
+ const choices = buildChoices(['alpha', 'beta']);
51
+ expect((0, fuzzySearchPrompt_1.fuzzyFilterChoices)(choices, 'zzz')).toEqual([]);
52
+ });
53
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightdash/cli",
3
- "version": "2.185.3",
3
+ "version": "2.193.1",
4
4
  "description": "Lightdash CLI tool",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -22,6 +22,8 @@
22
22
  "@actions/core": "1.11.1",
23
23
  "@aws-sdk/credential-providers": "3.995.0",
24
24
  "@casl/ability": "6.8.0",
25
+ "@lightdash/common": "2.193.1",
26
+ "@lightdash/warehouses": "2.193.1",
25
27
  "@sidvind/better-ajv-errors": "7.0.0",
26
28
  "@types/columnify": "1.5.1",
27
29
  "ajv": "8.18.0",
@@ -33,6 +35,7 @@
33
35
  "execa": "5.1.1",
34
36
  "google-auth-library": "10.7.0",
35
37
  "inquirer": "8.2.7",
38
+ "inquirer-autocomplete-prompt": "2.0.1",
36
39
  "js-yaml": "4.3.0",
37
40
  "lodash": "4.18.1",
38
41
  "node-fetch": "2.7.0",
@@ -44,14 +47,11 @@
44
47
  "tar-stream": "3.1.8",
45
48
  "unique-names-generator": "4.7.1",
46
49
  "uuid": "11.1.1",
47
- "yaml": "2.8.3",
48
- "@lightdash/common": "2.185.3",
49
- "@lightdash/warehouses": "2.185.3"
50
+ "yaml": "2.8.3"
50
51
  },
51
52
  "devDependencies": {
52
53
  "@types/inquirer": "8.2.4",
53
- "oxlint": "1.75.0",
54
- "oxlint-tsgolint": "7.0.2001",
54
+ "@types/inquirer-autocomplete-prompt": "3.0.3",
55
55
  "@types/js-yaml": "4.0.9",
56
56
  "@types/lodash": "4.14.202",
57
57
  "@types/node-fetch": "2.6.13",
@@ -62,6 +62,8 @@
62
62
  "@vercel/ncc": "0.38.4",
63
63
  "@yao-pkg/pkg": "6.7.0",
64
64
  "copyfiles": "2.4.1",
65
+ "oxlint": "1.75.0",
66
+ "oxlint-tsgolint": "7.0.2001",
65
67
  "tsx": "4.19.4",
66
68
  "typescript": "7.0.2",
67
69
  "vitest": "4.1.6"
@@ -73,7 +75,7 @@
73
75
  "build": "tsc --build tsconfig.json",
74
76
  "postbuild": "copyfiles --all -u 5 \"../../sandboxes/data-apps/template/**/*\" dist/vendor/template && copyfiles --all -u 4 \"src/handlers/apps/authoring/**/*\" dist/vendor/authoring",
75
77
  "bundle": "ncc build dist/index.js -o bundle -e @duckdb/node-api -e @duckdb/node-bindings -e @duckdb/node-bindings-darwin-arm64 -e @duckdb/node-bindings-darwin-x64 -e @duckdb/node-bindings-linux-arm64 -e @duckdb/node-bindings-linux-x64 -e @duckdb/node-bindings-win32-x64 && bash ./scripts/copy-bundle-assets.sh",
76
- "build:binary": "DUCKDB_BINDINGS=node-bindings-darwin-x64,node-bindings-darwin-arm64 pnpm bundle && npx @yao-pkg/pkg bundle/index.js --config pkg.config.json --targets node24-macos-x64,node24-macos-arm64 --output bin/lightdash --compress Brotli",
78
+ "build:binary": "DUCKDB_BINDINGS=node-bindings-darwin-x64,node-bindings-darwin-arm64 pnpm bundle && pnpm exec pkg bundle/index.js --config pkg.config.json --targets node24-macos-x64,node24-macos-arm64 --output bin/lightdash --compress Brotli",
77
79
  "typecheck": "tsc --project tsconfig.json --noEmit",
78
80
  "linter": "oxlint -c .oxlintrc.json --ignore-path ./../../.gitignore",
79
81
  "formatter": "oxfmt",