@lightdash/cli 1.251.7 → 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.
@@ -1 +1 @@
1
- {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/dbt/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAA+B,MAAM,mBAAmB,CAAC;AAQ7E,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAErD,MAAM,MAAM,gBAAgB,GAAG;IAC3B,SAAS,EAAE,MAAM,CAAC;CACrB,CAAC;AAsBF,eAAO,MAAM,YAAY,mBAEtB,gBAAgB,KAAG,OAAO,CAAC,WAAW,CAGxC,CAAC;AAEF,eAAO,MAAM,SAAS,UAAW,MAAM,KAAG,OAOzC,CAAC;AAiBF,eAAO,MAAM,mBAAmB,cACjB,MAAM,KAClB,OAAO,CAAC,WAAW,CAKrB,CAAC"}
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/dbt/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,WAAW,EAId,MAAM,mBAAmB,CAAC;AAQ3B,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAErD,MAAM,MAAM,gBAAgB,GAAG;IAC3B,SAAS,EAAE,MAAM,CAAC;CACrB,CAAC;AAsBF,eAAO,MAAM,YAAY,mBAEtB,gBAAgB,KAAG,OAAO,CAAC,WAAW,CAGxC,CAAC;AAEF,eAAO,MAAM,SAAS,UAAW,MAAM,KAAG,OAOzC,CAAC;AAoEF,eAAO,MAAM,mBAAmB,cACjB,MAAM,KAClB,OAAO,CAAC,WAAW,CAKrB,CAAC"}
@@ -40,14 +40,46 @@ const isHttpUrl = (value) => {
40
40
  }
41
41
  };
42
42
  exports.isHttpUrl = isHttpUrl;
43
+ const isGenericUnexpectedServerError = (responseText) => {
44
+ try {
45
+ const response = JSON.parse(responseText);
46
+ if (!(0, common_1.isApiError)(response))
47
+ return false;
48
+ const { error } = response;
49
+ return (error.statusCode === 500 &&
50
+ error.name === 'UnexpectedServerError' &&
51
+ error.message === 'Something went wrong.');
52
+ }
53
+ catch {
54
+ return false;
55
+ }
56
+ };
57
+ const isLightdashFrontendFallback = (url, response, responseText) => {
58
+ const { pathname } = new URL(url);
59
+ const isFrontendPath = pathname !== '/api' && !pathname.startsWith('/api/');
60
+ const isLightdashResponse = response.headers.get('lightdash-version') !== null;
61
+ const isHtmlResponse = response.headers.get('content-type')?.includes('text/html') === true ||
62
+ /^\s*(?:<!doctype html|<html)/i.test(responseText);
63
+ return (isFrontendPath &&
64
+ isLightdashResponse &&
65
+ (isHtmlResponse || isGenericUnexpectedServerError(responseText)));
66
+ };
43
67
  const loadManifestFromUrl = async (url) => {
44
68
  globalState_1.default.debug(`> Loading dbt manifest from ${url}`);
45
69
  try {
46
70
  const response = await (0, node_fetch_1.default)(url);
71
+ const responseText = await response.text();
72
+ const isFrontendFallback = isLightdashFrontendFallback(url, response, responseText);
47
73
  if (!response.ok) {
74
+ if (isFrontendFallback) {
75
+ throw new Error('Manifest not found: the URL resolves to a Lightdash app route instead of a dbt manifest');
76
+ }
48
77
  throw new Error(`${response.status} ${response.statusText}`);
49
78
  }
50
- const manifest = JSON.parse(await response.text());
79
+ if (isFrontendFallback) {
80
+ throw new Error('Manifest not found: the URL resolves to a Lightdash app route instead of a dbt manifest');
81
+ }
82
+ const manifest = JSON.parse(responseText);
51
83
  return manifest;
52
84
  }
53
85
  catch (err) {
@@ -50,6 +50,9 @@ describe('loadCombineManifest', () => {
50
50
  });
51
51
  mockedFetch.mockResolvedValue({
52
52
  ok: true,
53
+ headers: {
54
+ get: (name) => name === 'content-type' ? 'application/json' : null,
55
+ },
53
56
  text: async () => JSON.stringify(manifest),
54
57
  });
55
58
  const result = await (0, manifest_1.loadCombineManifest)('https://example.com/manifest.json');
@@ -62,10 +65,98 @@ describe('loadCombineManifest', () => {
62
65
  ok: false,
63
66
  status: 404,
64
67
  statusText: 'Not Found',
68
+ headers: {
69
+ get: () => 'application/json',
70
+ },
65
71
  text: async () => '',
66
72
  });
67
73
  await expect((0, manifest_1.loadCombineManifest)('https://example.com/manifest.json')).rejects.toThrow('Could not load manifest from https://example.com/manifest.json');
68
74
  });
75
+ test('reports the Lightdash dev app fallback as a missing manifest', async () => {
76
+ mockedFetch.mockResolvedValue({
77
+ ok: false,
78
+ status: 500,
79
+ statusText: 'Internal Server Error',
80
+ headers: {
81
+ get: (name) => {
82
+ if (name === 'content-type') {
83
+ return 'application/json; charset=utf-8';
84
+ }
85
+ if (name === 'lightdash-version')
86
+ return '1.163.2';
87
+ return null;
88
+ },
89
+ },
90
+ text: async () => JSON.stringify({
91
+ status: 'error',
92
+ error: {
93
+ statusCode: 500,
94
+ name: 'UnexpectedServerError',
95
+ message: 'Something went wrong.',
96
+ data: {},
97
+ },
98
+ }),
99
+ });
100
+ const promise = (0, manifest_1.loadCombineManifest)('http://localhost:8080/charter5-does-not-exist/manifest.json');
101
+ await expect(promise).rejects.toThrow('Manifest not found');
102
+ await expect(promise).rejects.not.toThrow('500 Internal Server Error');
103
+ });
104
+ test('reports the Lightdash production html fallback as a missing manifest', async () => {
105
+ mockedFetch.mockResolvedValue({
106
+ ok: true,
107
+ status: 200,
108
+ statusText: 'OK',
109
+ headers: {
110
+ get: (name) => {
111
+ if (name === 'content-type') {
112
+ return 'text/html; charset=utf-8';
113
+ }
114
+ if (name === 'lightdash-version')
115
+ return '1.163.2';
116
+ return null;
117
+ },
118
+ },
119
+ text: async () => '<!DOCTYPE html><html><body><div id="root"></div></body></html>',
120
+ });
121
+ await expect((0, manifest_1.loadCombineManifest)('https://lightdash.example.com/missing/manifest.json')).rejects.toThrow('Manifest not found');
122
+ });
123
+ test.each([
124
+ [
125
+ 'a Lightdash API error',
126
+ 'https://lightdash.example.com/api/v1/missing/manifest.json',
127
+ '1.163.2',
128
+ ],
129
+ [
130
+ 'an unmarked external error',
131
+ 'https://example.com/missing/manifest.json',
132
+ null,
133
+ ],
134
+ ])('preserves %s', async (_description, url, lightdashVersion) => {
135
+ mockedFetch.mockResolvedValue({
136
+ ok: false,
137
+ status: 500,
138
+ statusText: 'Internal Server Error',
139
+ headers: {
140
+ get: (name) => {
141
+ if (name === 'content-type')
142
+ return 'application/json';
143
+ if (name === 'lightdash-version')
144
+ return lightdashVersion;
145
+ return null;
146
+ },
147
+ },
148
+ text: async () => JSON.stringify({
149
+ status: 'error',
150
+ error: {
151
+ statusCode: 500,
152
+ name: 'UnexpectedServerError',
153
+ message: 'Something went wrong.',
154
+ data: {},
155
+ },
156
+ }),
157
+ });
158
+ await expect((0, manifest_1.loadCombineManifest)(url)).rejects.toThrow('500 Internal Server Error');
159
+ });
69
160
  test('reads a manifest from a local path', async () => {
70
161
  const manifest = buildManifest({
71
162
  'model.proj.b': modelNode('model.proj.b'),
@@ -4,6 +4,6 @@ type DbtModelsGroupedByState = {
4
4
  invalid: ExploreError[];
5
5
  skipped: DbtRawModelNode[];
6
6
  };
7
- export declare const validateDbtModel: (adapterType: string, manifestVersion: DbtManifestVersion, models: DbtRawModelNode[]) => Promise<DbtModelsGroupedByState>;
7
+ export declare const validateDbtModel: (adapterType: string, manifestVersion: DbtManifestVersion, models: DbtRawModelNode[], servedModelIds?: ReadonlySet<string>) => Promise<DbtModelsGroupedByState>;
8
8
  export {};
9
9
  //# sourceMappingURL=validation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/dbt/validation.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,YAAY,EACZ,eAAe,EACf,YAAY,EAOZ,KAAK,kBAAkB,EAC1B,MAAM,mBAAmB,CAAC;AAG3B,KAAK,uBAAuB,GAAG;IAC3B,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,OAAO,EAAE,eAAe,EAAE,CAAC;CAC9B,CAAC;AACF,eAAO,MAAM,gBAAgB,gBACZ,MAAM,mBACF,kBAAkB,UAC3B,eAAe,EAAE,KAC1B,OAAO,CAAC,uBAAuB,CAqEjC,CAAC"}
1
+ {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/dbt/validation.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,YAAY,EACZ,eAAe,EACf,YAAY,EAOZ,KAAK,kBAAkB,EAC1B,MAAM,mBAAmB,CAAC;AAG3B,KAAK,uBAAuB,GAAG;IAC3B,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,OAAO,EAAE,eAAe,EAAE,CAAC;CAC9B,CAAC;AAaF,eAAO,MAAM,gBAAgB,gBACZ,MAAM,mBACF,kBAAkB,UAC3B,eAAe,EAAE,mBACT,WAAW,CAAC,MAAM,CAAC,KACpC,OAAO,CAAC,uBAAuB,CAuEjC,CAAC"}
@@ -4,7 +4,14 @@ exports.validateDbtModel = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const common_1 = require("@lightdash/common");
6
6
  const globalState_1 = tslib_1.__importDefault(require("../globalState"));
7
- const validateDbtModel = async (adapterType, manifestVersion, models) => {
7
+ const formatServedManifestValidationError = (message) => {
8
+ const details = message
9
+ .replace(/ (?:Remove or update it|Update this field) in your dbt model, then deploy again\./g, '')
10
+ .trimEnd();
11
+ const separator = /[.!?]$/.test(details) ? ' ' : '. ';
12
+ return `${details}${separator}The served manifest is outdated. Redeploy the Lightdash project, then try again.`;
13
+ };
14
+ const validateDbtModel = async (adapterType, manifestVersion, models, servedModelIds = new Set()) => {
8
15
  globalState_1.default.debug(`> Validating ${models.length} models from dbt manifest`);
9
16
  globalState_1.default.debug(`> Validating models using dbt manifest version ${manifestVersion}`);
10
17
  const validator = new common_1.ManifestValidator(manifestVersion);
@@ -15,7 +22,9 @@ const validateDbtModel = async (adapterType, manifestVersion, models) => {
15
22
  if (!isValid) {
16
23
  error = {
17
24
  type: common_1.InlineErrorType.METADATA_PARSE_ERROR,
18
- message: errorMessage,
25
+ message: servedModelIds.has(model.unique_id)
26
+ ? formatServedManifestValidationError(errorMessage)
27
+ : errorMessage,
19
28
  };
20
29
  }
21
30
  else if (isValid && Object.values(model.columns).length <= 0) {
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const common_1 = require("@lightdash/common");
4
4
  const validation_1 = require("./validation");
5
5
  const makeModel = (name) => ({
6
+ unique_id: `model.test.${name}`,
6
7
  name,
7
8
  resource_type: 'model',
8
9
  database: 'database',
@@ -10,6 +11,26 @@ const makeModel = (name) => ({
10
11
  meta: {},
11
12
  });
12
13
  describe('validateDbtModel', () => {
14
+ afterEach(() => {
15
+ vi.restoreAllMocks();
16
+ });
17
+ test('directs served manifest validation failures to a project redeploy', async () => {
18
+ const validationMessage = 'Model "served_orders" from dbt source "server": Field at model root must have required property \'original_file_path\'. Update this field in your dbt model, then deploy again.';
19
+ const invalidValidator = Object.assign(vi.fn().mockReturnValue(false), {
20
+ errors: [],
21
+ });
22
+ vi.spyOn(common_1.ManifestValidator, 'getValidator').mockReturnValue(invalidValidator);
23
+ vi.spyOn(common_1.ManifestValidator, 'formatAjvErrors').mockReturnValue(validationMessage);
24
+ const servedModel = makeModel('served_orders');
25
+ const localModel = makeModel('local_orders');
26
+ const result = await (0, validation_1.validateDbtModel)('postgres', common_1.DbtManifestVersion.V11, [servedModel, localModel], new Set([servedModel.unique_id]));
27
+ const servedMessage = result.invalid[0].errors[0].message;
28
+ const localMessage = result.invalid[1].errors[0].message;
29
+ expect(servedMessage).toContain('The served manifest is outdated.');
30
+ expect(servedMessage).toContain('Redeploy the Lightdash project, then try again.');
31
+ expect(servedMessage).not.toContain('Update this field in your dbt model');
32
+ expect(localMessage).toContain('Update this field in your dbt model');
33
+ });
13
34
  test('keeps valid v12 models when column config metadata is invalid', async () => {
14
35
  const validModel = makeModel('valid_orders');
15
36
  const invalidModel = {
@@ -12,6 +12,8 @@ export type CompileHandlerOptions = DbtCompileOptions & {
12
12
  disableTimestampConversion?: boolean;
13
13
  validateWarehouseColumns?: boolean;
14
14
  partialCompilation?: boolean;
15
+ combineManifestProjectUuid?: string;
16
+ combine?: boolean;
15
17
  };
16
18
  export type CompileProjectResult = {
17
19
  explores: (Explore | ExploreError)[];
@@ -1 +1 @@
1
- {"version":3,"file":"compile.d.ts","sourceRoot":"","sources":["../../src/handlers/compile.ts"],"names":[],"mappings":"AAAA,OAAO,EAOH,OAAO,EACP,YAAY,EAef,MAAM,mBAAmB,CAAC;AAoB3B,OAAO,EAAE,iBAAiB,EAA8B,MAAM,eAAe,CAAC;AAI9E,MAAM,MAAM,qBAAqB,GAAG,iBAAiB,GAAG;IACpD,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,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IAC/B,QAAQ,EAAE,CAAC,OAAO,GAAG,YAAY,CAAC,EAAE,CAAC;IACrC,iBAAiB,EAAE,OAAO,CAAC;CAC9B,CAAC;AAEF,eAAO,MAAM,uBAAuB,YACvB,OAAO,GAAG,YAAY,KAChC,OAIW,CAAC;AAEf,eAAO,MAAM,0BAA0B,YAC1B,OAAO,GAAG,YAAY,KAChC,OAAO,GAAG,YAiBZ,CAAC;AAuMF,eAAO,MAAM,cAAc,YACd,qBAAqB,KAC/B,OAAO,CAAC,oBAAoB,CAsU9B,CAAC;AAEF,eAAO,MAAM,OAAO,YACP,qBAAqB,KAC/B,OAAO,CAAC,CAAC,OAAO,GAAG,YAAY,CAAC,EAAE,CACO,CAAC;AAE7C,eAAO,MAAM,cAAc,oBACN,qBAAqB,kBAmCzC,CAAC"}
1
+ {"version":3,"file":"compile.d.ts","sourceRoot":"","sources":["../../src/handlers/compile.ts"],"names":[],"mappings":"AAAA,OAAO,EAOH,OAAO,EACP,YAAY,EAgBf,MAAM,mBAAmB,CAAC;AAqB3B,OAAO,EAAE,iBAAiB,EAA8B,MAAM,eAAe,CAAC;AAI9E,MAAM,MAAM,qBAAqB,GAAG,iBAAiB,GAAG;IACpD,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,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,0BAA0B,CAAC,EAAE,MAAM,CAAC;IACpC,OAAO,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IAC/B,QAAQ,EAAE,CAAC,OAAO,GAAG,YAAY,CAAC,EAAE,CAAC;IACrC,iBAAiB,EAAE,OAAO,CAAC;CAC9B,CAAC;AAEF,eAAO,MAAM,uBAAuB,YACvB,OAAO,GAAG,YAAY,KAChC,OAIW,CAAC;AAEf,eAAO,MAAM,0BAA0B,YAC1B,OAAO,GAAG,YAAY,KAChC,OAAO,GAAG,YAiBZ,CAAC;AA6TF,eAAO,MAAM,cAAc,YACd,qBAAqB,KAC/B,OAAO,CAAC,oBAAoB,CA0a9B,CAAC;AAEF,eAAO,MAAM,OAAO,YACP,qBAAqB,KAC/B,OAAO,CAAC,CAAC,OAAO,GAAG,YAAY,CAAC,EAAE,CACO,CAAC;AAE7C,eAAO,MAAM,cAAc,oBACN,qBAAqB,kBAmCzC,CAAC"}
@@ -15,6 +15,7 @@ const lightdash_config_1 = require("../lightdash-config");
15
15
  const loader_1 = require("../lightdash/loader");
16
16
  const projectType_1 = require("../lightdash/projectType");
17
17
  const styles = tslib_1.__importStar(require("../styles"));
18
+ const apiClient_1 = require("./dbt/apiClient");
18
19
  const compile_1 = require("./dbt/compile");
19
20
  const getDbtVersion_1 = require("./dbt/getDbtVersion");
20
21
  const getWarehouseClient_1 = tslib_1.__importDefault(require("./dbt/getWarehouseClient"));
@@ -35,6 +36,64 @@ const stripWarehouseColumnErrors = (explore) => {
35
36
  : exploreWithoutWarnings;
36
37
  };
37
38
  exports.stripWarehouseColumnErrors = stripWarehouseColumnErrors;
39
+ const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
40
+ const isServedDbtManifest = (value) => isRecord(value) && isRecord(value.nodes) && isRecord(value.metadata);
41
+ const getManifestModelIds = (manifest) => Object.entries(manifest.nodes)
42
+ .filter(([, node]) => node.resource_type === 'model')
43
+ .map(([uniqueId]) => uniqueId);
44
+ const getCompiledManifestModelIds = (manifest) => Object.entries(manifest.nodes)
45
+ .filter(([, node]) => node.resource_type === 'model' &&
46
+ node
47
+ .compiled === true)
48
+ .map(([uniqueId]) => uniqueId);
49
+ const inferLocalSourceName = (localManifest, servedManifest) => {
50
+ const sourceNames = new Set(getManifestModelIds(localManifest).flatMap((uniqueId) => {
51
+ const servedNode = servedManifest.nodes[uniqueId];
52
+ return servedNode?.resource_type === 'model' &&
53
+ typeof servedNode.lightdash_source_name === 'string'
54
+ ? [servedNode.lightdash_source_name]
55
+ : [];
56
+ }));
57
+ if (sourceNames.size > 1) {
58
+ throw new common_1.ParseError(`Cannot automatically combine manifest from the server: overlapping local models match multiple Lightdash sources (${[...sourceNames].sort().join(', ')})`);
59
+ }
60
+ return sourceNames.values().next().value;
61
+ };
62
+ const withoutModelsFromSource = (manifest, sourceName) => ({
63
+ ...manifest,
64
+ nodes: Object.fromEntries(Object.entries(manifest.nodes).filter(([, node]) => {
65
+ const sourceAnnotatedNode = node;
66
+ return !(node.resource_type === 'model' &&
67
+ sourceAnnotatedNode.lightdash_source_name === sourceName);
68
+ })),
69
+ });
70
+ const withoutUnselectedOverlappingModels = (localManifest, servedManifest, sourceName, selectedModelIds) => ({
71
+ ...localManifest,
72
+ nodes: Object.fromEntries(Object.entries(localManifest.nodes).filter(([uniqueId, localNode]) => {
73
+ const servedNode = servedManifest.nodes[uniqueId];
74
+ return !(localNode.resource_type === 'model' &&
75
+ !selectedModelIds.has(uniqueId) &&
76
+ servedNode?.resource_type === 'model' &&
77
+ servedNode.lightdash_source_name === sourceName);
78
+ })),
79
+ });
80
+ const combinePreviewManifests = (localManifest, externalManifest, localSourceName) => {
81
+ const { manifest, addedModelIds } = (0, manifest_1.combineManifests)(localManifest, externalManifest);
82
+ const nodes = { ...manifest.nodes };
83
+ Object.entries(localManifest.nodes).forEach(([uniqueId, localNode]) => {
84
+ const externalNode = externalManifest.nodes[uniqueId];
85
+ const sourceName = localNode.resource_type === 'model' && localSourceName !== undefined
86
+ ? localSourceName
87
+ : externalNode?.lightdash_source_name;
88
+ if (typeof sourceName === 'string') {
89
+ nodes[uniqueId] = {
90
+ ...nodes[uniqueId],
91
+ lightdash_source_name: sourceName,
92
+ };
93
+ }
94
+ });
95
+ return { manifest: { ...manifest, nodes }, addedModelIds };
96
+ };
38
97
  const getDisplayableDiagnostics = (explore, allowPartialCompilation) => {
39
98
  const diagnostics = explore.warnings ?? [];
40
99
  const errors = diagnostics.filter((diagnostic) => diagnostic.type === common_1.InlineErrorType.WAREHOUSE_COLUMN_ERROR);
@@ -200,18 +259,90 @@ const compileProject = async (options) => {
200
259
  (0, common_1.getCompiledModels)(projectManifestModels, compiledModelIds)
201
260
  .length === projectManifestModels.length;
202
261
  let effectiveCompiledModelIds = compiledModelIds;
262
+ const servedModelIds = new Set();
263
+ let additionalManifest;
264
+ let combineSource;
265
+ let isAutomaticServerManifest = false;
203
266
  if (options.combineManifest) {
204
- const externalManifest = await (0, manifest_1.loadCombineManifest)(options.combineManifest);
205
- const { manifest: merged, addedModelIds } = (0, manifest_1.combineManifests)(manifest, externalManifest);
206
- manifest = merged;
207
- if (effectiveCompiledModelIds !== undefined &&
208
- addedModelIds.length > 0) {
209
- effectiveCompiledModelIds = [
210
- ...effectiveCompiledModelIds,
211
- ...addedModelIds,
212
- ];
267
+ additionalManifest = await (0, manifest_1.loadCombineManifest)(options.combineManifest);
268
+ combineSource = `external manifest from ${options.combineManifest}`;
269
+ }
270
+ else if (options.combine !== false &&
271
+ options.combineManifestProjectUuid) {
272
+ try {
273
+ const manifestEndpoint = `/api/v1/projects/${options.combineManifestProjectUuid}/dbt/manifest`;
274
+ const response = await (0, apiClient_1.lightdashRawApi)({
275
+ method: 'GET',
276
+ url: manifestEndpoint,
277
+ body: undefined,
278
+ });
279
+ const servedManifest = await response.json();
280
+ if (!isServedDbtManifest(servedManifest)) {
281
+ throw new Error(`${manifestEndpoint} returned an invalid manifest: expected an object with metadata and a nodes record`);
282
+ }
283
+ additionalManifest = servedManifest;
284
+ combineSource = 'manifest from the server';
285
+ isAutomaticServerManifest = true;
286
+ }
287
+ catch (error) {
288
+ if (error instanceof common_1.LightdashError &&
289
+ error.statusCode === 404) {
290
+ console.info(styles.info('No server manifest found; continuing with the preview manifest'));
291
+ }
292
+ else {
293
+ console.error(styles.warning(`Could not fetch the server manifest; continuing with the preview manifest: ${(0, common_1.getErrorMessage)(error)}`));
294
+ }
295
+ }
296
+ }
297
+ if (additionalManifest && combineSource) {
298
+ const localSourceName = isAutomaticServerManifest
299
+ ? inferLocalSourceName(manifest, additionalManifest)
300
+ : undefined;
301
+ if (isAutomaticServerManifest && localSourceName === undefined) {
302
+ console.info(styles.info(`Skipped combining ${combineSource}: the local dbt project is not a source of this Lightdash project`));
303
+ }
304
+ else {
305
+ const localManifest = isAutomaticServerManifest &&
306
+ !isProjectComplete &&
307
+ localSourceName !== undefined
308
+ ? withoutUnselectedOverlappingModels(manifest, additionalManifest, localSourceName, new Set(originallySelectedModelIds ??
309
+ compiledModelIds ??
310
+ []))
311
+ : manifest;
312
+ const externalManifest = isAutomaticServerManifest &&
313
+ isProjectComplete &&
314
+ localSourceName !== undefined
315
+ ? withoutModelsFromSource(additionalManifest, localSourceName)
316
+ : additionalManifest;
317
+ const compiledExternalModelIds = getCompiledManifestModelIds(additionalManifest);
318
+ const { manifest: merged, addedModelIds } = combinePreviewManifests(localManifest, externalManifest, localSourceName);
319
+ manifest = merged;
320
+ if (isAutomaticServerManifest) {
321
+ addedModelIds.forEach((modelId) => {
322
+ servedModelIds.add(modelId);
323
+ });
324
+ }
325
+ if (effectiveCompiledModelIds !== undefined &&
326
+ addedModelIds.length > 0) {
327
+ effectiveCompiledModelIds = [
328
+ ...effectiveCompiledModelIds,
329
+ ...addedModelIds,
330
+ ];
331
+ }
332
+ let combineResult;
333
+ if (addedModelIds.length > 0) {
334
+ combineResult = `added ${addedModelIds.length} model(s) not present in the preview manifest`;
335
+ }
336
+ else if (compiledExternalModelIds.length === 0) {
337
+ combineResult =
338
+ 'added 0 model(s) because the manifest contains no compiled models';
339
+ }
340
+ else {
341
+ combineResult =
342
+ 'added 0 model(s) because all compiled models already exist in the preview manifest';
343
+ }
344
+ console.info(styles.info(`Combined ${combineSource}: ${combineResult}`));
213
345
  }
214
- console.info(styles.info(`Combined external manifest from ${options.combineManifest}: added ${addedModelIds.length} model(s) not present in the preview manifest`));
215
346
  }
216
347
  const manifestVersion = (0, common_1.getDbtManifestVersion)(manifest);
217
348
  const manifestModels = (0, common_1.getModelsFromManifest)(manifest);
@@ -223,7 +354,7 @@ const compileProject = async (options) => {
223
354
  ? await patchDeferredModels(compiledModels, originallySelectedModelIds, options.state)
224
355
  : compiledModels;
225
356
  const adapterType = manifest.metadata.adapter_type;
226
- const { valid: validModels, invalid: failedExplores } = await (0, validation_1.validateDbtModel)(adapterType, manifestVersion, modelsForValidation);
357
+ const { valid: validModels, invalid: failedExplores } = await (0, validation_1.validateDbtModel)(adapterType, manifestVersion, modelsForValidation, servedModelIds);
227
358
  if (failedExplores.length > 0) {
228
359
  const errors = failedExplores.map((failedExplore) => failedExplore.errors.map((error) => `- ${failedExplore.name}: ${error.message}\n`));
229
360
  console.error(styles.warning(`Found ${failedExplores.length} errors when validating dbt models: