@narrative.io/data-collaboration-sdk-ts 3.0.1 → 3.1.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.
package/README.md CHANGED
@@ -317,91 +317,67 @@ For detailed API documentation, please visit the [Narrative.io API Documentation
317
317
 
318
318
  ## Local Development: Use This SDK Locally in a Consumer App (No Publish Needed)
319
319
 
320
- You can link this SDK into another repo (e.g., a Nuxt 3 app) and iterate without publishing to npm each time. Two supported flows:
320
+ Link this SDK into another local app (e.g. a Nuxt 3 project) and iterate without publishing to npm. Both modes consume the SDK's built output in `build/**`, exactly like the published package — the only difference is how the bytes reach the app's `node_modules`:
321
321
 
322
- - **Linked mode (fastest)** — symlink your local SDK into the app
323
- - **Tarball mode (exact publish parity)** — install from a local `npm pack` tarball
322
+ - **Linked mode (default)** — `bun link` symlinks this repo into the app; with `tsc -w` running, `src/**` edits flow live.
323
+ - **Tarball mode** — `bun pm pack` → the app installs the `.tgz` (exact publish parity, a snapshot).
324
324
 
325
- Both approaches use the SDK’s **built output** in `build/`, exactly like the published package.
325
+ The full flow (build → link/pack → install → verify resolution) is automated by two `bun run` scripts (backed by `scripts/link-local.sh` and `scripts/unlink-local.sh`). Both default the consumer app to `~/narrative/narrative-platform-ui` (override with `--app PATH` or `$NARRATIVE_UI_DIR`). Pass flags after a `--` so `bun` forwards them to the script.
326
326
 
327
- ### Prerequisites
327
+ ### `bun run` scripts
328
328
 
329
- - Bun (this project uses bun as its package manager)
330
- - This repo builds TypeScript to `build/`
331
- - This repo includes `scripts/ensureBuild.js` to verify artifacts exist
329
+ **Set up — `bun run link:local`** (build → register/pack → install into the app → verify it resolves into this repo):
332
330
 
333
331
  ```bash
334
- # In the SDK repo (this one)
335
- bun install
336
- bun run build # one-time; emits build/**
337
- node scripts/ensureBuild.js
332
+ # Linked mode into the default app, then verify resolution
333
+ bun run link:local
334
+
335
+ # A different consumer app
336
+ bun run link:local -- --app /abs/path/to/app
337
+
338
+ # Tarball mode (exact publish parity)
339
+ bun run link:local -- --mode tarball
340
+
341
+ # SDK side only (build + register/pack); prints the app-side command to run by hand
342
+ bun run link:local -- --sdk-only
338
343
  ```
339
344
 
340
- ---
341
-
342
- ### Option A — Linked Mode (Fastest Dev Feedback)
343
-
344
- 1. **Keep the SDK building on save**
345
- In the SDK repo (leave this running):
346
- ```bash
347
- bun run dev # tsc -w → continuously updates build/**
348
- ```
349
-
350
- 2. **Register the SDK globally**
351
- In the SDK repo (new terminal):
352
- ```bash
353
- bun run link:global # runs ensureBuild + bun link
354
- ```
355
-
356
- 3. **Link into your consumer app**
357
- In your app repo (e.g., Nuxt project):
358
- ```bash
359
- # link the globally-registered SDK into this app's node_modules
360
- bun link @narrative.io/data-collaboration-sdk-ts
361
- ```
362
-
363
- 4. **Verify the app resolves to your local SDK**
364
- ```bash
365
- node -e "console.log(require.resolve('@narrative.io/data-collaboration-sdk-ts/package.json'))"
366
- ```
367
- You should see a path inside your SDK repo (not a versioned folder under the app's `node_modules`).
368
-
369
- 5. **Run your app**
370
- ```bash
371
- bun run dev
372
- ```
373
- Edit files under `SDK/src/**` → the watcher writes to `build/**` → your app hot-reloads.
374
-
375
- **Revert to the registry version (in the app):**
345
+ For live edits in linked mode, keep the watcher running in this repo so `src/**` edits rebuild `build/**`:
346
+
376
347
  ```bash
377
- bun remove @narrative.io/data-collaboration-sdk-ts
378
- bun add @narrative.io/data-collaboration-sdk-ts
348
+ bun run dev # tsc -w → continuously rewrites build/**
379
349
  ```
380
350
 
381
- ---
351
+ **Tear down — `bun run unlink:local`** (stop the watcher → unlink from the app and reinstall the published package → deregister the global link):
382
352
 
383
- ### Option B — Tarball Mode (Exact Publish Parity)
353
+ ```bash
354
+ # Undo everything for the default app
355
+ bun run unlink:local
356
+
357
+ # Preview without changing anything
358
+ bun run unlink:local -- --dry-run
359
+ ```
384
360
 
385
- 1. **Pack the SDK (same artifact you would publish)**
386
- ```bash
387
- # in the SDK repo
388
- bun run build
389
- bun run pack:dist # creates ./<name>-<version>.tgz
390
- ```
361
+ Flags: `--app PATH`, `--no-reinstall` (leave the SDK uninstalled), `--keep-global-link`, `--no-kill-watcher`, `--dry-run`.
391
362
 
392
- 2. **Install the tarball in the app**
393
- ```bash
394
- # in the app repo
395
- bun add /ABS/PATH/TO/<name>-<version>.tgz
396
- bun run dev
397
- ```
363
+ ### Lowest level (plain bun, no scripts)
398
364
 
399
- **Revert to a registry version (in the app):**
400
365
  ```bash
401
- bun add @narrative.io/data-collaboration-sdk-ts@<semver>
402
- ```
366
+ # Linked mode — SDK repo, then the app
367
+ bun install && bun run build && bun run link:global
368
+ bun link @narrative.io/data-collaboration-sdk-ts # in the app
369
+
370
+ # Tarball mode — SDK repo, then the app
371
+ bun run build && bun run pack:dist # → ./<name>-<version>.tgz
372
+ bun add /ABS/PATH/TO/<name>-<version>.tgz # in the app
403
373
 
404
- ---
374
+ # Verify from the app — must resolve into this SDK repo (linked) or the tarball
375
+ node -e "console.log(require.resolve('@narrative.io/data-collaboration-sdk-ts/package.json'))"
376
+
377
+ # Revert (in the app)
378
+ bun remove @narrative.io/data-collaboration-sdk-ts
379
+ bun add @narrative.io/data-collaboration-sdk-ts # or @<semver>
380
+ ```
405
381
 
406
382
  ### Troubleshooting (Nuxt 3 / Vite / SSR)
407
383
 
@@ -413,36 +389,8 @@ bun add @narrative.io/data-collaboration-sdk-ts@<semver>
413
389
  })
414
390
  ```
415
391
 
416
- - **Build artifacts not updating:** Ensure `npm run dev` is running in the SDK repo (TypeScript watch), and that the app is linked (or reinstalled from a fresh tarball).
392
+ - **Build artifacts not updating:** Ensure `bun run dev` is running in the SDK repo (TypeScript watch), and that the app is linked (or reinstalled from a fresh tarball).
417
393
 
418
394
  - **Accidental imports from `src/*`:** Consumers should import from the package root only. This SDK publishes `build/**` and defines `main`/`types` (and may include an `exports` map) to prevent `src/*` imports.
419
395
 
420
- - **Type/version drift (e.g., Zod):** If your app and the SDK use different major versions of a library whose types appear in public APIs, align versions or declare that library as a `peerDependency` in the SDK.
421
-
422
- ---
423
-
424
- ### Quick Reference
425
-
426
- ```bash
427
- # SDK (this repo)
428
- bun install
429
- bun run build
430
- bun run dev
431
- bun run link:global
432
-
433
- # App (consumer repo)
434
- bun link @narrative.io/data-collaboration-sdk-ts
435
- bun run dev
436
- ```
437
-
438
- Alternative (publish-parity):
439
-
440
- ```bash
441
- # SDK
442
- bun run build
443
- bun run pack:dist
444
-
445
- # App
446
- bun add /ABS/PATH/TO/*.tgz
447
- bun run dev
448
- ```
396
+ - **Type/version drift (e.g., Zod):** If your app and the SDK use different major versions of a library whose types appear in public APIs, align versions or declare that library as a `peerDependency` in the SDK.
@@ -1,10 +1,43 @@
1
1
  import { BaseApi } from "../base-api";
2
2
  import type { ComputePoolResponse, CreateComputePoolRequest, ListComputePoolsParams, ListComputePoolsResponse, UpdateComputePoolRequest } from "./types";
3
+ /**
4
+ * A class for accessing the Compute Pools API.
5
+ *
6
+ * @extends BaseApi
7
+ */
3
8
  export declare class ComputePoolsApi extends BaseApi {
4
9
  private computePoolsResourcePath;
10
+ /**
11
+ * List compute pools for a data plane.
12
+ *
13
+ * Note: the public API documents a `status` query parameter, but the
14
+ * backend currently returns active compute pools regardless of that value.
15
+ *
16
+ * @see {@link https://docs.narrative.io/api-reference/compute-pools/list-compute-pools}
17
+ */
5
18
  listComputePools(dataPlaneId: string, params?: ListComputePoolsParams): Promise<ListComputePoolsResponse>;
19
+ /**
20
+ * Create a compute pool for a data plane.
21
+ *
22
+ * @see {@link https://docs.narrative.io/api-reference/compute-pools/create-a-compute-pool}
23
+ */
6
24
  createComputePool(dataPlaneId: string, request: CreateComputePoolRequest): Promise<ComputePoolResponse>;
25
+ /**
26
+ * Get a compute pool by ID.
27
+ *
28
+ * @see {@link https://docs.narrative.io/api-reference/compute-pools/get-a-compute-pool-by-id}
29
+ */
7
30
  getComputePool(dataPlaneId: string, computePoolId: string): Promise<ComputePoolResponse>;
31
+ /**
32
+ * Update a compute pool.
33
+ *
34
+ * @see {@link https://docs.narrative.io/api-reference/compute-pools/update-a-compute-pool}
35
+ */
8
36
  updateComputePool(dataPlaneId: string, computePoolId: string, request: UpdateComputePoolRequest): Promise<ComputePoolResponse>;
37
+ /**
38
+ * Archive a compute pool.
39
+ *
40
+ * @see {@link https://docs.narrative.io/api-reference/compute-pools/archive-a-compute-pool}
41
+ */
9
42
  archiveComputePool(dataPlaneId: string, computePoolId: string): Promise<void>;
10
43
  }
@@ -1,20 +1,53 @@
1
1
  import { BaseApi } from "../base-api";
2
+ /**
3
+ * A class for accessing the Compute Pools API.
4
+ *
5
+ * @extends BaseApi
6
+ */
2
7
  export class ComputePoolsApi extends BaseApi {
3
8
  computePoolsResourcePath(dataPlaneId) {
4
9
  return `data-planes/${dataPlaneId}/compute-pools`;
5
10
  }
11
+ /**
12
+ * List compute pools for a data plane.
13
+ *
14
+ * Note: the public API documents a `status` query parameter, but the
15
+ * backend currently returns active compute pools regardless of that value.
16
+ *
17
+ * @see {@link https://docs.narrative.io/api-reference/compute-pools/list-compute-pools}
18
+ */
6
19
  async listComputePools(dataPlaneId, params) {
7
20
  return await this.get(this.computePoolsResourcePath(dataPlaneId), params);
8
21
  }
22
+ /**
23
+ * Create a compute pool for a data plane.
24
+ *
25
+ * @see {@link https://docs.narrative.io/api-reference/compute-pools/create-a-compute-pool}
26
+ */
9
27
  async createComputePool(dataPlaneId, request) {
10
28
  return await this.post(this.computePoolsResourcePath(dataPlaneId), request);
11
29
  }
30
+ /**
31
+ * Get a compute pool by ID.
32
+ *
33
+ * @see {@link https://docs.narrative.io/api-reference/compute-pools/get-a-compute-pool-by-id}
34
+ */
12
35
  async getComputePool(dataPlaneId, computePoolId) {
13
36
  return await this.get(`${this.computePoolsResourcePath(dataPlaneId)}/${computePoolId}`);
14
37
  }
38
+ /**
39
+ * Update a compute pool.
40
+ *
41
+ * @see {@link https://docs.narrative.io/api-reference/compute-pools/update-a-compute-pool}
42
+ */
15
43
  async updateComputePool(dataPlaneId, computePoolId, request) {
16
44
  return await this.put(`${this.computePoolsResourcePath(dataPlaneId)}/${computePoolId}`, request);
17
45
  }
46
+ /**
47
+ * Archive a compute pool.
48
+ *
49
+ * @see {@link https://docs.narrative.io/api-reference/compute-pools/archive-a-compute-pool}
50
+ */
18
51
  async archiveComputePool(dataPlaneId, computePoolId) {
19
52
  await this.delete(`${this.computePoolsResourcePath(dataPlaneId)}/${computePoolId}`);
20
53
  }
@@ -1,6 +1,8 @@
1
1
  export type ComputePoolType = "owned" | "shared";
2
2
  export type ComputePoolStatus = "active" | "archived";
3
- export type SnowflakeWarehouseSize = "x-small" | "small" | "medium" | "large" | "x-large" | "2x-large" | "3x-large" | "4x-large" | "5x-large" | "6x-large";
3
+ export type ComputePoolSize = "x_small" | "small" | "medium" | "large" | "x_large" | "2x_large" | "3x_large" | "4x_large" | "5x_large" | "6x_large";
4
+ export type SnowflakeWarehouseSize = ComputePoolSize;
5
+ export type AwsEmrSize = ComputePoolSize;
4
6
  export type SnowflakeWarehouseType = "standard" | "snowpark_optimized";
5
7
  export type ScalingPolicy = "standard" | "economy";
6
8
  export interface SnowflakeWarehouseExternalId {
@@ -8,9 +10,13 @@ export interface SnowflakeWarehouseExternalId {
8
10
  name: string;
9
11
  alias: string;
10
12
  }
11
- export type ComputePoolExternalId = SnowflakeWarehouseExternalId;
13
+ export interface AwsEmrExternalId {
14
+ type: "aws_emr";
15
+ }
16
+ export type ComputePoolExternalId = SnowflakeWarehouseExternalId | AwsEmrExternalId;
12
17
  export interface SnowflakeWarehouseProvider {
13
18
  type: "snowflake_warehouse";
19
+ warehouse_name?: string;
14
20
  size?: SnowflakeWarehouseSize;
15
21
  warehouse_type?: SnowflakeWarehouseType;
16
22
  auto_suspend?: number;
@@ -26,7 +32,13 @@ export interface SnowflakeWarehouseProvider {
26
32
  resource_constraint?: string;
27
33
  comment?: string;
28
34
  }
29
- export type ComputePoolProvider = SnowflakeWarehouseProvider;
35
+ export interface AwsEmrProvider {
36
+ type: "aws_emr";
37
+ size: AwsEmrSize;
38
+ idle_timeout_seconds?: number;
39
+ job_execution_timeout_seconds?: number;
40
+ }
41
+ export type ComputePoolProvider = SnowflakeWarehouseProvider | AwsEmrProvider;
30
42
  export interface ParticipantsAll {
31
43
  type: "all";
32
44
  }
@@ -73,15 +85,22 @@ export interface ComputePoolSharedResponse {
73
85
  provider: ComputePoolProvider;
74
86
  }
75
87
  export type ComputePoolResponse = ComputePoolOwnedResponse | ComputePoolSharedResponse;
76
- export interface CreateComputePoolRequest {
77
- external_id: ComputePoolExternalId;
88
+ export interface CreateComputePoolRequestCommon {
78
89
  name: string;
79
90
  display_name: string;
80
91
  description?: string;
81
92
  tags?: string[];
82
93
  collaborators?: ComputePoolCollaborators;
83
- provider: ComputePoolProvider;
84
94
  }
95
+ export type CreateSnowflakeWarehouseComputePoolRequest = CreateComputePoolRequestCommon & {
96
+ external_id: SnowflakeWarehouseExternalId;
97
+ provider: SnowflakeWarehouseProvider;
98
+ };
99
+ export type CreateAwsEmrComputePoolRequest = CreateComputePoolRequestCommon & {
100
+ external_id?: AwsEmrExternalId;
101
+ provider: AwsEmrProvider;
102
+ };
103
+ export type CreateComputePoolRequest = CreateSnowflakeWarehouseComputePoolRequest | CreateAwsEmrComputePoolRequest;
85
104
  export interface UpdateComputePoolRequest {
86
105
  display_name?: string;
87
106
  description?: string;
@@ -0,0 +1,75 @@
1
+ import type { AwsEmrExternalId, AwsEmrProvider, ComputePoolResponse, CreateComputePoolRequest, SnowflakeWarehouseExternalId, SnowflakeWarehouseProvider } from "./types";
2
+ type IsAssignable<T, U> = T extends U ? true : false;
3
+ type AssertTrue<T extends true> = T;
4
+ type AssertFalse<T extends false> = T;
5
+ export type AwsCreateWithoutExternalId = AssertTrue<IsAssignable<{
6
+ name: "aws_pool";
7
+ display_name: "AWS pool";
8
+ provider: {
9
+ type: "aws_emr";
10
+ size: "x_small";
11
+ };
12
+ }, CreateComputePoolRequest>>;
13
+ export type AwsCreateWithExternalId = AssertTrue<IsAssignable<{
14
+ name: "aws_pool";
15
+ display_name: "AWS pool";
16
+ external_id: {
17
+ type: "aws_emr";
18
+ };
19
+ provider: {
20
+ type: "aws_emr";
21
+ size: "x_small";
22
+ };
23
+ }, CreateComputePoolRequest>>;
24
+ export type SnowflakeCreateWithoutExternalId = AssertFalse<IsAssignable<{
25
+ name: "snowflake_pool";
26
+ display_name: "Snowflake pool";
27
+ provider: {
28
+ type: "snowflake_warehouse";
29
+ };
30
+ }, CreateComputePoolRequest>>;
31
+ export type SnowflakeCreateWithExternalId = AssertTrue<IsAssignable<{
32
+ name: "snowflake_pool";
33
+ display_name: "Snowflake pool";
34
+ external_id: {
35
+ type: "snowflake_warehouse";
36
+ name: "WAREHOUSE_NAME";
37
+ alias: "3c90c3cc-0d44-4b50-8888-8dd25736052a";
38
+ };
39
+ provider: {
40
+ type: "snowflake_warehouse";
41
+ warehouse_name: "WAREHOUSE_NAME";
42
+ };
43
+ }, CreateComputePoolRequest>>;
44
+ export type AwsResponse = AssertTrue<IsAssignable<{
45
+ type: "owned";
46
+ id: string;
47
+ company_id: number;
48
+ data_plane_id: string;
49
+ external_id: AwsEmrExternalId;
50
+ name: string;
51
+ display_name: string;
52
+ status: "active";
53
+ created_at: string;
54
+ tags: string[];
55
+ provider: AwsEmrProvider;
56
+ collaborators: {
57
+ use: {
58
+ type: "none";
59
+ };
60
+ };
61
+ }, ComputePoolResponse>>;
62
+ export type SnowflakeResponse = AssertTrue<IsAssignable<{
63
+ type: "shared";
64
+ id: string;
65
+ company_id: number;
66
+ data_plane_id: string;
67
+ external_id: SnowflakeWarehouseExternalId;
68
+ name: string;
69
+ display_name: string;
70
+ status: "active";
71
+ created_at: string;
72
+ tags: string[];
73
+ provider: SnowflakeWarehouseProvider;
74
+ }, ComputePoolResponse>>;
75
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -32,14 +32,86 @@ export function parseExplain(n) {
32
32
  return nqlOrFail(n);
33
33
  }
34
34
  }
35
+ function isRecord(value) {
36
+ return typeof value === "object" && value !== null && !Array.isArray(value);
37
+ }
38
+ function walkNodes(value, visit) {
39
+ if (Array.isArray(value)) {
40
+ for (const item of value)
41
+ walkNodes(item, visit);
42
+ return;
43
+ }
44
+ if (isRecord(value)) {
45
+ visit(value);
46
+ for (const key of Object.keys(value))
47
+ walkNodes(value[key], visit);
48
+ }
49
+ }
50
+ // The server qualifies first-party (`company_data`) refs by dataset NAME when a
51
+ // query was written by name (e.g. `company_data.my_dataset`), echoing the name in
52
+ // the `table` field and carrying the numeric dataset id only in a table node's
53
+ // `metadata.id`. `parseTable`/`parseColumnRef` bind a dataset node only when
54
+ // `table` is the numeric id, so we first rewrite name-qualified first-party refs
55
+ // back to their id: build a name/alias -> id map from the FROM tables' metadata,
56
+ // then rewrite both the table refs and the column refs that point at them. Id-form
57
+ // queries are unaffected, and non-`company_data` schemas (e.g. third-party access
58
+ // rules `<slug>.<name>`) are left untouched.
59
+ function resolveFirstPartyDatasetId(node) {
60
+ const table = node.table;
61
+ if (typeof table === "string" && /^\d+$/.test(table)) {
62
+ return Number.parseInt(table, 10);
63
+ }
64
+ const metadata = node.metadata;
65
+ if (isRecord(metadata)) {
66
+ const id = metadata.id;
67
+ if (typeof id === "number")
68
+ return id;
69
+ if (typeof id === "string" && /^\d+$/.test(id)) {
70
+ return Number.parseInt(id, 10);
71
+ }
72
+ }
73
+ return undefined;
74
+ }
75
+ function normalizeFirstPartyRefs(n) {
76
+ const clone = JSON.parse(JSON.stringify(n));
77
+ const idByRef = new Map();
78
+ // Pass 1: first-party FROM tables — record name/alias -> id, rewrite to id.
79
+ walkNodes(clone, (node) => {
80
+ if (node.type !== "table" || node.schema !== "company_data")
81
+ return;
82
+ const id = resolveFirstPartyDatasetId(node);
83
+ if (id === undefined)
84
+ return;
85
+ if (typeof node.table === "string")
86
+ idByRef.set(node.table, id);
87
+ if (typeof node.as === "string")
88
+ idByRef.set(node.as, id);
89
+ idByRef.set(String(id), id);
90
+ node.table = String(id);
91
+ });
92
+ if (idByRef.size === 0)
93
+ return clone;
94
+ // Pass 2: first-party column refs still qualified by name -> rewrite to id.
95
+ walkNodes(clone, (node) => {
96
+ if (node.type !== "column" || node.schema !== "company_data")
97
+ return;
98
+ if (typeof node.table !== "string" || /^\d+$/.test(node.table))
99
+ return;
100
+ const id = idByRef.get(node.table);
101
+ if (id !== undefined)
102
+ node.table = String(id);
103
+ });
104
+ return clone;
105
+ }
35
106
  export function parseSelect(n) {
107
+ const normalized = normalizeFirstPartyRefs(n);
36
108
  const select = {
37
109
  type: "select",
38
- budget: n.budget,
39
- columns: n.columns.map((col) => parseOutput(col)),
40
- deduplication: parseDeduplication(n),
41
- from: n.from !== null ? parseFrom(n.from) : [],
42
- where: n.where !== null ? parseWhere(n.where) : null,
110
+ budget: normalized.budget,
111
+ columns: normalized.columns.map((col) => parseOutput(col)),
112
+ deduplication: parseDeduplication(normalized),
113
+ from: normalized.from !== null ? parseFrom(normalized.from) : [],
114
+ where: normalized.where !== null ? parseWhere(normalized.where) : null,
43
115
  };
44
116
  return select;
45
117
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narrative.io/data-collaboration-sdk-ts",
3
- "version": "3.0.1",
3
+ "version": "3.1.1",
4
4
  "main": "build/index.js",
5
5
  "repository": "github:narrative-io/data-collaboration-sdk-ts",
6
6
  "source": "src/index.ts",
@@ -19,7 +19,9 @@
19
19
  "dev": "tsc -w",
20
20
  "check:built": "node scripts/ensureBuild.js",
21
21
  "link:global": "bun run check:built && bun link",
22
- "pack:dist": "bun run check:built && bun pm pack"
22
+ "pack:dist": "bun run check:built && bun pm pack",
23
+ "link:local": "bash scripts/link-local.sh",
24
+ "unlink:local": "bash scripts/unlink-local.sh"
23
25
  },
24
26
  "keywords": [],
25
27
  "author": "",
@@ -41,7 +43,7 @@
41
43
  },
42
44
  "dependencies": {
43
45
  "zod": "4.4.3",
44
- "bignumber.js": "11.1.2"
46
+ "bignumber.js": "11.1.3"
45
47
  },
46
48
  "overrides": {
47
49
  "semver@<7.5.2": "7.5.2"