@lightdash/cli 1.15.0 → 1.17.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":"connectSnowflake.d.ts","sourceRoot":"","sources":["../../src/handlers/connectSnowflake.ts"],"names":[],"mappings":"AAAA,OAAO,EAKH,KAAK,0BAA0B,EAIlC,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAEH,wBAAwB,EAG3B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAwB,MAAM,YAAY,CAAC;AAIlD,MAAM,MAAM,uBAAuB,GAAG;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,KAAK,yBAAyB,GAAG,IAAI,CACjC,wBAAwB,EACtB,2BAA2B,GAC3B,+BAA+B,GAC/B,qBAAqB,GACrB,uBAAuB,GACvB,0BAA0B,GAC1B,+BAA+B,GAC/B,kBAAkB,GAClB,oBAAoB,CACzB,CAAC;AAEF,KAAK,gBAAgB,GAAG;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,KAAK,4BAA4B,GAAG;IAChC,sBAAsB,EAAE,CACpB,WAAW,EAAE,0BAA0B,KACtC,yBAAyB,CAAC;IAC/B,eAAe,EAAE,MAAM,gBAAgB,CAAC;IACxC,KAAK,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,GAAG,EAAE,MAAM,IAAI,CAAC;IAChB,KAAK,EAAE,OAAO,KAAK,CAAC;IACpB,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC,CAAC;AA0XF,eAAO,MAAM,uBAAuB,GAChC,SAAS,uBAAuB,EAChC,eAAc,4BAAkD,KACjE,OAAO,CAAC,IAAI,CAwEd,CAAC"}
1
+ {"version":3,"file":"connectSnowflake.d.ts","sourceRoot":"","sources":["../../src/handlers/connectSnowflake.ts"],"names":[],"mappings":"AAAA,OAAO,EAKH,KAAK,0BAA0B,EAIlC,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAEH,wBAAwB,EAG3B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAwB,MAAM,YAAY,CAAC;AAIlD,MAAM,MAAM,uBAAuB,GAAG;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,KAAK,yBAAyB,GAAG,IAAI,CACjC,wBAAwB,EACtB,2BAA2B,GAC3B,+BAA+B,GAC/B,qBAAqB,GACrB,uBAAuB,GACvB,0BAA0B,GAC1B,+BAA+B,GAC/B,kBAAkB,GAClB,oBAAoB,CACzB,CAAC;AAEF,KAAK,gBAAgB,GAAG;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,KAAK,4BAA4B,GAAG;IAChC,sBAAsB,EAAE,CACpB,WAAW,EAAE,0BAA0B,KACtC,yBAAyB,CAAC;IAC/B,eAAe,EAAE,MAAM,gBAAgB,CAAC;IACxC,KAAK,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,GAAG,EAAE,MAAM,IAAI,CAAC;IAChB,KAAK,EAAE,OAAO,KAAK,CAAC;IACpB,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC,CAAC;AA6XF,eAAO,MAAM,uBAAuB,GAChC,SAAS,uBAAuB,EAChC,eAAc,4BAAkD,KACjE,OAAO,CAAC,IAAI,CAwEd,CAAC"}
@@ -8,7 +8,7 @@ const node_fetch_1 = tslib_1.__importDefault(require("node-fetch"));
8
8
  const node_crypto_1 = require("node:crypto");
9
9
  const analytics_1 = require("../analytics/analytics");
10
10
  const PAT_EXPIRY_DAYS = 365;
11
- const PAT_NETWORK_POLICY_BYPASS_MINUTES = 1440;
11
+ const PAT_NETWORK_POLICY_BYPASS_MINUTES = 60;
12
12
  const KEY_VERIFICATION_ATTEMPTS = 3;
13
13
  const KEY_VERIFICATION_RETRY_DELAY_MS = 5_000;
14
14
  const generateSnowflakeKeyPair = () => {
@@ -235,6 +235,7 @@ const createDurableCredential = async (authorizationCodeCredentials, warehouseCl
235
235
  const { tokenSecret } = await warehouseClient.createProgrammaticAccessToken(`LIGHTDASH_ONBOARDING_${Date.now()}`, PAT_EXPIRY_DAYS, PAT_NETWORK_POLICY_BYPASS_MINUTES);
236
236
  const expiresAt = new Date(dependencies.now().getTime() +
237
237
  PAT_EXPIRY_DAYS * 24 * 60 * 60 * 1000);
238
+ dependencies.write(`The token bypasses your Snowflake network policy for the next ${PAT_NETWORK_POLICY_BYPASS_MINUTES} minutes so this connection can be verified. Allowlist Lightdash's IP addresses in your network policy to keep it working afterwards.`);
238
239
  return {
239
240
  credentials: {
240
241
  type: common_1.WarehouseTypes.SNOWFLAKE,
@@ -291,7 +291,7 @@ describe('connectSnowflakeHandler', () => {
291
291
  });
292
292
  await expect((0, connectSnowflake_1.connectSnowflakeHandler)(options, getDependencies())).resolves.toBeUndefined();
293
293
  expect(setUserPublicKey).not.toHaveBeenCalled();
294
- expect(createProgrammaticAccessToken).toHaveBeenCalledWith(expect.stringMatching(/^LIGHTDASH_ONBOARDING_\d+$/), 365, 1440);
294
+ expect(createProgrammaticAccessToken).toHaveBeenCalledWith(expect.stringMatching(/^LIGHTDASH_ONBOARDING_\d+$/), 365, 60);
295
295
  const request = {
296
296
  code: options.code,
297
297
  credentials: {
@@ -321,8 +321,9 @@ describe('connectSnowflakeHandler', () => {
321
321
  it('falls back to a PAT when key registration lacks privileges', async () => {
322
322
  vi.mocked(setUserPublicKey).mockRejectedValue(new Error('Insufficient privileges to operate on user'));
323
323
  await expect((0, connectSnowflake_1.connectSnowflakeHandler)(options, getDependencies())).resolves.toBeUndefined();
324
- expect(createProgrammaticAccessToken).toHaveBeenCalledWith(expect.stringMatching(/^LIGHTDASH_ONBOARDING_\d+$/), 365, 1440);
324
+ expect(createProgrammaticAccessToken).toHaveBeenCalledWith(expect.stringMatching(/^LIGHTDASH_ONBOARDING_\d+$/), 365, 60);
325
325
  expect(write).toHaveBeenCalledWith('Secured with a programmatic access token (expires 2027-07-14)');
326
+ expect(write).toHaveBeenCalledWith(expect.stringContaining('bypasses your Snowflake network policy for the next 60 minutes'));
326
327
  });
327
328
  it('prints administrator remediation when both durable paths fail', async () => {
328
329
  vi.mocked(setUserPublicKey).mockRejectedValue(new Error('Insufficient privileges'));
@@ -53,7 +53,7 @@
53
53
  "@vitejs/plugin-react": "6.0.1",
54
54
  "autoprefixer": "10.4.20",
55
55
  "oxc-parser": "0.128.0",
56
- "postcss": "8.5.10",
56
+ "postcss": "8.5.18",
57
57
  "tailwindcss": "3.4.17",
58
58
  "typescript": "6.0.3",
59
59
  "vite": "8.0.5"
@@ -34,10 +34,10 @@ Each file contains:
34
34
 
35
35
  **How to use these:**
36
36
  1. Read the JSON file(s) to understand what data the user wants in their app
37
- 2. Cross-reference the field IDs against the dbt YAML catalog at
38
- `/tmp/dbt-repo/models/schema.yml` for field descriptions, types, and relationships
39
- between fields — this helps you understand how the referenced charts connect to the
40
- user's overall prompt
37
+ 2. Cross-reference the field IDs against the dbt YAML catalog — read
38
+ `/tmp/dbt-repo/models/<exploreName>.yml` for the referenced explores — for field
39
+ descriptions, types, and relationships between fields; this helps you understand how
40
+ the referenced charts connect to the user's overall prompt
41
41
  3. Map fields to SDK calls:
42
42
  - `chartName` → `query(exploreName).label(chartName)` — **always set the label**
43
43
  - `metricQuery.dimensions` → `.dimensions([...])`
@@ -49,7 +49,17 @@ Available at `@/components/ui/<name>`:
49
49
 
50
50
  ## Semantic Layer (dbt models)
51
51
 
52
- The available data models are defined in dbt YAML files at **`/tmp/dbt-repo/models/`**. Read these to discover every model, dimension, metric, join, and parameter available to you. **Never guess field names** — use only what's in the YAML. When a dimension or metric has `ai_hints`, follow that guidance when deciding which field best matches the user's intent; hints supplement names, labels, and descriptions. (Parameters live in a `parameters:` block under `meta:` / `config.meta:`, or in `lightdash.config.yml` — see [Parameters](#parameters).)
52
+ The available data models are defined in dbt YAML files at **`/tmp/dbt-repo/models/`** — one file per model, plus an index. **Never guess field names** — use only what's in the YAML. When a dimension or metric has `ai_hints`, follow that guidance when deciding which field best matches the user's intent; hints supplement names, labels, and descriptions. (Parameters live in a `parameters:` block under `meta:` / `config.meta:`, or in `lightdash.config.yml` — see [Parameters](#parameters).)
53
+
54
+ ### Finding the right models
55
+
56
+ Projects range from a handful of models to well over a thousand, so the directory is indexed rather than inlined:
57
+
58
+ 1. **Read `/tmp/dbt-repo/models/_index.md` first.** It lists every model, most-queried first, with its file name, dimension/metric counts, joined tables, and description.
59
+ 2. **Read only the model files the app actually needs**, e.g. `Read /tmp/dbt-repo/models/orders.yml`. Each file holds that model's complete dimensions, metrics, joins, and parameters.
60
+ 3. **Grep when the index isn't enough.** If you know a field name but not its model, `Grep` the directory for it. A wide model may be split across `<name>.yml`, `<name>.part2.yml`, … — the last line of each part points at the next.
61
+
62
+ **Never read every model file, and never page through a file with `offset`/`limit`** — pick the model from the index and read that one file whole.
53
63
 
54
64
  ### Reading dbt YAML
55
65
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightdash/cli",
3
- "version": "1.15.0",
3
+ "version": "1.17.0",
4
4
  "description": "Lightdash CLI tool",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -41,8 +41,8 @@
41
41
  "unique-names-generator": "4.7.1",
42
42
  "uuid": "11.1.1",
43
43
  "yaml": "2.8.3",
44
- "@lightdash/common": "1.15.0",
45
- "@lightdash/warehouses": "1.15.0"
44
+ "@lightdash/common": "1.17.0",
45
+ "@lightdash/warehouses": "1.17.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/inquirer": "8.2.4",
@@ -1,5 +0,0 @@
1
- export declare const getOrganizationContentFileNames: ({ values, fallbackPrefix, }: {
2
- values: string[];
3
- fallbackPrefix: string;
4
- }) => string[];
5
- //# sourceMappingURL=fileNames.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"fileNames.d.ts","sourceRoot":"","sources":["../../../src/handlers/organizationContent/fileNames.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,+BAA+B,GAAI,6BAG7C;IACC,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;CAC1B,KAAG,MAAM,EAOJ,CAAC"}
@@ -1,12 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getOrganizationContentFileNames = void 0;
4
- const fileNames_1 = require("../contentAsCode/fileNames");
5
- const getOrganizationContentFileNames = ({ values, fallbackPrefix, }) => (0, fileNames_1.allocateContentFileNames)({
6
- items: values.map((value) => ({
7
- identity: value,
8
- displayName: value,
9
- })),
10
- fallbackPrefix,
11
- });
12
- exports.getOrganizationContentFileNames = getOrganizationContentFileNames;
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=fileNames.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"fileNames.test.d.ts","sourceRoot":"","sources":["../../../src/handlers/organizationContent/fileNames.test.ts"],"names":[],"mappings":""}
@@ -1,36 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const crypto_1 = require("crypto");
4
- const fileNames_1 = require("./fileNames");
5
- describe('organization content filenames', () => {
6
- it('uses generateSlug with normalized accents', () => {
7
- expect((0, fileNames_1.getOrganizationContentFileNames)({
8
- values: ['Málaga Finance'],
9
- fallbackPrefix: 'group',
10
- })).toStrictEqual(['malaga-finance.yml']);
11
- });
12
- it('uses a deterministic hash when no slug can be generated', () => {
13
- const value = '🎉';
14
- const hash = (0, crypto_1.createHash)('sha256')
15
- .update(value)
16
- .digest('hex')
17
- .slice(0, 8);
18
- expect((0, fileNames_1.getOrganizationContentFileNames)({
19
- values: [value],
20
- fallbackPrefix: 'group',
21
- })).toStrictEqual([`group-${hash}.yml`]);
22
- });
23
- it('adds stable hashes when different values generate the same slug', () => {
24
- const values = ['Finance?', 'Finance!'];
25
- expect((0, fileNames_1.getOrganizationContentFileNames)({
26
- values,
27
- fallbackPrefix: 'group',
28
- })).toStrictEqual(values.map((value) => {
29
- const hash = (0, crypto_1.createHash)('sha256')
30
- .update(value)
31
- .digest('hex')
32
- .slice(0, 8);
33
- return `finance-${hash}.yml`;
34
- }));
35
- });
36
- });