@embeddable.com/sdk-core 3.3.1 → 3.4.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.
@@ -0,0 +1,184 @@
1
+ import push from "./push";
2
+ import provideConfig from "./provideConfig";
3
+ import { fileFromPath } from "formdata-node/file-from-path";
4
+ import * as archiver from "archiver";
5
+ import * as fs from "node:fs/promises";
6
+ import * as fsSync from "node:fs";
7
+ import { findFiles } from "@embeddable.com/sdk-utils";
8
+
9
+ // @ts-ignore
10
+ import reportErrorToRollbar from "./rollbar.mjs";
11
+ import { checkBuildSuccess, checkNodeVersion, getArgumentByKey } from "./utils";
12
+ import { server } from "../../../mocks/server";
13
+ import { http, HttpResponse } from "msw";
14
+
15
+ const infoMock = {
16
+ info: vi.fn(),
17
+ succeed: vi.fn(),
18
+ fail: vi.fn(),
19
+ };
20
+
21
+ const startMock = {
22
+ succeed: vi.fn(),
23
+ info: () => infoMock,
24
+ fail: vi.fn(),
25
+ };
26
+
27
+ vi.mock("ora", () => ({
28
+ default: () => ({
29
+ start: vi.fn().mockReturnValue(startMock),
30
+ info: vi.fn(),
31
+ }),
32
+ }));
33
+
34
+ vi.mock("./utils", () => ({
35
+ checkNodeVersion: vi.fn(),
36
+ checkBuildSuccess: vi.fn(),
37
+ getArgumentByKey: vi.fn(),
38
+ }));
39
+
40
+ vi.mock("node:fs/promises", () => ({
41
+ writeFile: vi.fn(),
42
+ readFile: vi.fn(),
43
+ access: vi.fn(),
44
+ rm: vi.fn(),
45
+ }));
46
+
47
+ vi.mock("node:fs", () => ({
48
+ createWriteStream: vi.fn(),
49
+ }));
50
+
51
+ vi.mock("./provideConfig", () => ({
52
+ default: vi.fn(),
53
+ }));
54
+
55
+ vi.mock("./rollbar.mjs", () => ({
56
+ default: vi.fn(),
57
+ }));
58
+
59
+ vi.mock("@embeddable.com/sdk-utils", () => ({
60
+ findFiles: vi.fn(),
61
+ }));
62
+
63
+ vi.mock("archiver", () => ({
64
+ create: vi.fn(),
65
+ }));
66
+
67
+ vi.mock("formdata-node/file-from-path", () => ({
68
+ fileFromPath: vi.fn().mockReturnValue(new Blob([new ArrayBuffer(8)])),
69
+ }));
70
+
71
+ const config = {
72
+ client: {
73
+ rootDir: "rootDir",
74
+ buildDir: "buildDir",
75
+ archiveFile: "embeddable-build.zip",
76
+ },
77
+ pushBaseUrl: "pushBaseUrl",
78
+ previewBaseUrl: "previewBaseUrl",
79
+ };
80
+
81
+ describe("push", () => {
82
+ beforeEach(() => {
83
+ vi.mocked(checkNodeVersion).mockResolvedValue(true);
84
+ vi.mocked(checkBuildSuccess).mockResolvedValue(true);
85
+ vi.mocked(getArgumentByKey).mockReturnValue(undefined);
86
+ vi.mocked(provideConfig).mockResolvedValue(config);
87
+ vi.mocked(fs.access).mockResolvedValue(undefined);
88
+ vi.mocked(fs.readFile).mockImplementation(async () =>
89
+ Buffer.from(`{"access_token":"mocked-token"}`),
90
+ );
91
+
92
+ vi.mocked(findFiles).mockResolvedValue([["fileName", "filePath"]]);
93
+ vi.mocked(fileFromPath).mockReturnValue(
94
+ new Blob([new ArrayBuffer(8)]) as any,
95
+ );
96
+ vi.mocked(archiver.create).mockReturnValue({
97
+ finalize: vi.fn(),
98
+ pipe: vi.fn(),
99
+ directory: vi.fn(),
100
+ file: vi.fn(),
101
+ } as any);
102
+
103
+ vi.mocked(fsSync.createWriteStream).mockReturnValue({
104
+ on: (event: string, cb: () => void) => {
105
+ cb();
106
+ },
107
+ end: vi.fn(),
108
+ } as any);
109
+
110
+ vi.spyOn(process, "exit").mockImplementation(() => null as never);
111
+ });
112
+
113
+ it("should push the build", async () => {
114
+ await push();
115
+
116
+ expect(provideConfig).toHaveBeenCalled();
117
+ expect(checkNodeVersion).toHaveBeenCalled();
118
+ expect(checkBuildSuccess).toHaveBeenCalled();
119
+
120
+ expect(fs.access).toHaveBeenCalledWith(config.client.buildDir);
121
+
122
+ expect(archiver.create).toHaveBeenCalledWith("zip", {
123
+ zlib: { level: 9 },
124
+ });
125
+ expect(fsSync.createWriteStream).toHaveBeenCalledWith(
126
+ config.client.archiveFile,
127
+ );
128
+
129
+ // after publishing the file gets removed
130
+ expect(fs.rm).toHaveBeenCalledWith(config.client.archiveFile);
131
+
132
+ expect(infoMock.info).toHaveBeenCalledWith(
133
+ "Publishing to mocked-workspace-name using previewBaseUrl/workspace/mocked-workspace-id...",
134
+ );
135
+ expect(infoMock.succeed).toHaveBeenCalledWith(
136
+ "Published to mocked-workspace-name using previewBaseUrl/workspace/mocked-workspace-id",
137
+ );
138
+ });
139
+
140
+ it("should fail if there are no workspaces", async () => {
141
+ vi.spyOn(console, "error").mockImplementation(() => undefined);
142
+ server.use(
143
+ http.get("**/workspace", () => {
144
+ return HttpResponse.json([]);
145
+ }),
146
+ );
147
+
148
+ await push();
149
+
150
+ expect(startMock.fail).toHaveBeenCalledWith("No workspaces found");
151
+ expect(process.exit).toHaveBeenCalledWith(1);
152
+ expect(reportErrorToRollbar).toHaveBeenCalled();
153
+ });
154
+
155
+ it("should fail if the build is not successful", async () => {
156
+ vi.mocked(checkBuildSuccess).mockResolvedValue(false);
157
+
158
+ await push();
159
+
160
+ expect(process.exit).toHaveBeenCalledWith(1);
161
+
162
+ expect(console.error).toHaveBeenCalledWith(
163
+ "Build failed or not completed. Please run `embeddable:build` first.",
164
+ );
165
+ });
166
+
167
+ it("should push by api key provided in the arguments", async () => {
168
+ vi.mocked(getArgumentByKey).mockReturnValue("mocked-api-key");
169
+ Object.defineProperties(process, {
170
+ argv: {
171
+ value: [
172
+ "--api-key",
173
+ "mocked-api-key",
174
+ "--email",
175
+ "mocked-email@valid.com",
176
+ ],
177
+ },
178
+ });
179
+
180
+ await push();
181
+
182
+ expect(startMock.succeed).toHaveBeenCalledWith("Published using API key");
183
+ });
184
+ });
package/src/push.ts CHANGED
@@ -43,6 +43,9 @@ export default async () => {
43
43
  }
44
44
 
45
45
  const token = await verify(config);
46
+ spinnerPushing = ora()
47
+ .start()
48
+ .info("No API Key provided. Standard login will be used.");
46
49
 
47
50
  const { workspaceId, name: workspaceName } = await selectWorkspace(
48
51
  config,
@@ -52,9 +55,9 @@ export default async () => {
52
55
  const workspacePreviewUrl = `${config.previewBaseUrl}/workspace/${workspaceId}`;
53
56
 
54
57
  await buildArchive(config);
55
- spinnerPushing = ora(
58
+ spinnerPushing.info(
56
59
  `Publishing to ${workspaceName} using ${workspacePreviewUrl}...`,
57
- ).start();
60
+ );
58
61
 
59
62
  await sendBuild(config, { workspaceId, token });
60
63
  spinnerPushing.succeed(
@@ -0,0 +1,118 @@
1
+ import * as fs from "node:fs/promises";
2
+ import {
3
+ checkNodeVersion,
4
+ getArgumentByKey,
5
+ storeBuildSuccessFlag,
6
+ checkBuildSuccess,
7
+ SUCCESS_FLAG_FILE,
8
+ } from "./utils";
9
+
10
+ const startMock = {
11
+ succeed: vi.fn(),
12
+ fail: vi.fn(),
13
+ };
14
+
15
+ const failMock = vi.fn();
16
+
17
+ vi.mock("ora", () => ({
18
+ default: () => ({
19
+ start: vi.fn().mockImplementation(() => startMock),
20
+ info: vi.fn(),
21
+ fail: failMock,
22
+ }),
23
+ }));
24
+
25
+ vi.mock("../package.json", () => ({
26
+ engines: {
27
+ node: "14.x",
28
+ },
29
+ }));
30
+
31
+ vi.mock("fs/promises", () => ({
32
+ readFile: vi.fn(),
33
+ writeFile: vi.fn(),
34
+ access: vi.fn(),
35
+ mkdir: vi.fn(),
36
+ }));
37
+
38
+ describe("utils", () => {
39
+ describe("checkNodeVersion", () => {
40
+ it("should return true if the node version is greater than 14", async () => {
41
+ const result = await checkNodeVersion();
42
+
43
+ expect(result).toBe(true);
44
+ });
45
+
46
+ it("should fail if the node version is less than 16", async () => {
47
+ Object.defineProperty(process.versions, "node", {
48
+ value: "14.0",
49
+ configurable: true,
50
+ });
51
+
52
+ vi.mock("../package.json", () => ({
53
+ engines: {
54
+ node: "16.x",
55
+ },
56
+ }));
57
+
58
+ vi.spyOn(process, "exit").mockImplementation(() => null as never);
59
+
60
+ const result = await checkNodeVersion();
61
+
62
+ expect(failMock).toHaveBeenCalledWith({
63
+ text: "Node version 16.0 or higher is required. You are running 14.0.",
64
+ color: "red",
65
+ });
66
+
67
+ expect(result).toBe(undefined);
68
+ });
69
+ });
70
+
71
+ describe("getArgumentByKey", () => {
72
+ it("should return the value of the argument", () => {
73
+ process.argv = ["--email", "test@a.com", "--password", "123456"];
74
+ const result = getArgumentByKey("--email");
75
+
76
+ expect(result).toBe("test@a.com");
77
+
78
+ process.argv = ["-e", "test@a.com", "--password", "123456"];
79
+ const result2 = getArgumentByKey(["--email", "-e"]);
80
+
81
+ expect(result2).toBe("test@a.com");
82
+ });
83
+ });
84
+
85
+ describe("storeBuildSuccessFlag", () => {
86
+ it("should write a success flag file", async () => {
87
+ await storeBuildSuccessFlag();
88
+
89
+ expect(fs.writeFile).toHaveBeenCalledWith(SUCCESS_FLAG_FILE, "true");
90
+ });
91
+
92
+ it("should throw an error if the file write fails", async () => {
93
+ vi.mocked(fs.writeFile).mockImplementation(async () => {
94
+ throw new Error();
95
+ });
96
+
97
+ await expect(storeBuildSuccessFlag()).rejects.toThrow();
98
+ });
99
+ });
100
+
101
+ describe("checkBuildSuccess", () => {
102
+ it("should return true if the success flag file exists", async () => {
103
+ vi.mocked(fs.access).mockResolvedValue(undefined);
104
+
105
+ const result = await checkBuildSuccess();
106
+
107
+ expect(result).toBe(true);
108
+ });
109
+
110
+ it("should return false if the success flag file does not exist", async () => {
111
+ vi.mocked(fs.access).mockRejectedValue(undefined);
112
+
113
+ const result = await checkBuildSuccess();
114
+
115
+ expect(result).toBe(false);
116
+ });
117
+ });
118
+ });
package/src/utils.ts CHANGED
@@ -6,23 +6,28 @@ import path from "node:path";
6
6
  let ora: any;
7
7
  export const checkNodeVersion = async () => {
8
8
  ora = (await oraP).default;
9
- ora("Checking node version...");
9
+ const spinner = ora("Checking node version...");
10
10
  const [major, minor] = process.versions.node.split(".").map(Number);
11
11
 
12
- const engines = require("../package.json").engines.node;
12
+ const packageJson = await import("../package.json");
13
+ const {
14
+ engines: { node },
15
+ } = packageJson;
13
16
 
14
- const [minMajor, minMinor] = engines
17
+ const [minMajor, minMinor] = node
15
18
  .split(".")
16
19
  .map((v: string) => v.replace(/[^\d]/g, ""))
17
20
  .map(Number);
18
21
 
19
22
  if (major < minMajor || (major === minMajor && minor < minMinor)) {
20
- ora({
23
+ spinner.fail({
21
24
  text: `Node version ${minMajor}.${minMinor} or higher is required. You are running ${major}.${minor}.`,
22
25
  color: "red",
23
- }).fail();
26
+ });
24
27
 
25
28
  process.exit(1);
29
+ } else {
30
+ return true;
26
31
  }
27
32
  };
28
33
 
@@ -48,7 +53,7 @@ export const getArgumentByKey = (key: string | string[]) => {
48
53
  return index !== -1 ? process.argv[index + 1] : undefined;
49
54
  };
50
55
 
51
- const SUCCESS_FLAG_FILE = `${CREDENTIALS_DIR}/success`;
56
+ export const SUCCESS_FLAG_FILE = `${CREDENTIALS_DIR}/success`;
52
57
  /**
53
58
  * Store a flag in the credentials directory to indicate a successful build
54
59
  * This is used to determine if the build was successful or not
@@ -0,0 +1,124 @@
1
+ import { dataModelsValidation, securityContextValidation } from "./validate";
2
+ import * as fs from "node:fs/promises";
3
+
4
+ const startMock = {
5
+ succeed: vi.fn(),
6
+ fail: vi.fn(),
7
+ };
8
+
9
+ const failMock = vi.fn();
10
+
11
+ const validYaml = `cubes:
12
+ - name: customers
13
+ title: My customers
14
+ data_source: default
15
+ sql_table: public.customers
16
+
17
+ dimensions:
18
+ - name: id
19
+ sql: id
20
+ type: number
21
+ primary_key: true`;
22
+
23
+ const invalidYaml = `${validYaml}
24
+ measures:
25
+ - name: count
26
+ type: count
27
+ title: '# of customers'
28
+ - name: test
29
+ type: number
30
+ sql: {count} / 10.0`;
31
+
32
+ const securityContextYaml = `
33
+ - name: Example customer 1
34
+ securityContext:
35
+ country: United States
36
+ environment: default`;
37
+
38
+ vi.mock("ora", () => ({
39
+ default: () => ({
40
+ start: vi.fn().mockImplementation(() => startMock),
41
+ info: vi.fn(),
42
+ fail: failMock,
43
+ }),
44
+ }));
45
+
46
+ vi.mock("node:fs/promises", () => ({
47
+ readFile: vi.fn(),
48
+ writeFile: vi.fn(),
49
+ access: vi.fn(),
50
+ mkdir: vi.fn(),
51
+ }));
52
+
53
+ describe("validate", () => {
54
+ describe("dataModelsValidation", () => {
55
+ it("should return an empty array if the data models are valid", async () => {
56
+ vi.mocked(fs.readFile).mockImplementation(async () => {
57
+ return validYaml;
58
+ });
59
+ const filesList: [string, string][] = [
60
+ ["valid-cube.yaml", "path/to/file"],
61
+ ];
62
+ const result = await dataModelsValidation(filesList);
63
+ expect(result).toEqual([]);
64
+ });
65
+
66
+ it("should return an array of error messages if the data models are invalid", async () => {
67
+ vi.mocked(fs.readFile).mockImplementation(async () => {
68
+ return "";
69
+ });
70
+ const filesList: [string, string][] = [
71
+ ["invalid-cube.yaml", "path/to/file"],
72
+ ];
73
+ const result = await dataModelsValidation(filesList);
74
+ expect(result).toEqual([
75
+ "path/to/file: At least one cubes or views must be defined",
76
+ ]);
77
+ });
78
+
79
+ it("should return an array of error messages if the data models parsing fails", async () => {
80
+ vi.mocked(fs.readFile).mockImplementation(async () => {
81
+ return invalidYaml;
82
+ });
83
+ const filesList: [string, string][] = [
84
+ ["invalid-cube.yaml", "path/to/file"],
85
+ ];
86
+ const result = await dataModelsValidation(filesList);
87
+ expect(result).toMatchInlineSnapshot(`
88
+ [
89
+ "path/to/file: Unexpected scalar at node end at line 18, column 22:
90
+
91
+ sql: {count} / 10.0
92
+ ^^^^^^
93
+ ",
94
+ ]
95
+ `);
96
+ });
97
+ });
98
+
99
+ describe("securityContextValidation", () => {
100
+ it("should return an empty array if the security context is valid", async () => {
101
+ vi.mocked(fs.readFile).mockImplementation(async () => {
102
+ return securityContextYaml;
103
+ });
104
+ const filesList: [string, string][] = [
105
+ ["valid-security-context.json", "path/to/file"],
106
+ ];
107
+ const result = await securityContextValidation(filesList);
108
+ expect(result).toEqual([]);
109
+ });
110
+
111
+ it("should return an array of error messages if the security context is invalid", async () => {
112
+ vi.mocked(fs.readFile).mockImplementation(async () => {
113
+ return `${securityContextYaml} ${securityContextYaml}`;
114
+ });
115
+ const filesList: [string, string][] = [
116
+ ["invalid-security-context.json", "path/to/file"],
117
+ ];
118
+ const result = await securityContextValidation(filesList);
119
+ expect(result).toEqual([
120
+ 'path/to/file: security context with name "Example customer 1" already exists',
121
+ ]);
122
+ });
123
+ });
124
+ });
package/src/validate.ts CHANGED
@@ -2,24 +2,11 @@ import * as fs from "node:fs/promises";
2
2
  import * as YAML from "yaml";
3
3
  import { errorFormatter, findFiles } from "@embeddable.com/sdk-utils";
4
4
  import { z } from "zod";
5
+ import { checkNodeVersion } from "./utils";
5
6
 
6
7
  const CUBE_YAML_FILE_REGEX = /^(.*)\.cube\.ya?ml$/;
7
8
  const SECURITY_CONTEXT_FILE_REGEX = /^(.*)\.sc\.ya?ml$/;
8
9
 
9
- const checkNodeVersion = () => {
10
- const [major, minor] = process.versions.node.split(".").map(Number);
11
-
12
- const engines = require("../package.json").engines.node;
13
-
14
- const [minMajor, minMinor] = engines.split(".").map(Number);
15
-
16
- if (major < minMajor || (major === minMajor && minor < minMinor)) {
17
- throw new Error(
18
- `Node version ${minMajor}.${minMinor} or higher is required. You are running ${major}.${minor}.`,
19
- );
20
- }
21
- };
22
-
23
10
  export default async (ctx: any, exitIfInvalid = true) => {
24
11
  checkNodeVersion();
25
12
  const ora = (await import("ora")).default;
@@ -73,32 +60,35 @@ export async function dataModelsValidation(filesList: [string, string][]) {
73
60
  for (const [_, filePath] of filesList) {
74
61
  const fileContentRaw = await fs.readFile(filePath, "utf8");
75
62
 
76
- const cube = YAML.parse(fileContentRaw);
77
-
78
- if (!cube?.cubes && !cube?.views) {
79
- return [`${filePath}: At least one cubes or views must be defined`];
80
- }
63
+ try {
64
+ const cube = YAML.parse(fileContentRaw);
65
+ if (!cube?.cubes && !cube?.views) {
66
+ return [`${filePath}: At least one cubes or views must be defined`];
67
+ }
81
68
 
82
- const cubeModelSafeParse = cubeModelSchema.safeParse(cube);
83
- const viewModelSafeParse = viewModelSchema.safeParse(cube);
69
+ const cubeModelSafeParse = cubeModelSchema.safeParse(cube);
70
+ const viewModelSafeParse = viewModelSchema.safeParse(cube);
84
71
 
85
- if (cube.cubes && !cubeModelSafeParse.success) {
86
- errorFormatter(cubeModelSafeParse.error.issues).forEach((error) => {
87
- errors.push(`${filePath}: ${error}`);
88
- });
89
- }
72
+ if (cube.cubes && !cubeModelSafeParse.success) {
73
+ errorFormatter(cubeModelSafeParse.error.issues).forEach((error) => {
74
+ errors.push(`${filePath}: ${error}`);
75
+ });
76
+ }
90
77
 
91
- if (cube.views && !viewModelSafeParse.success) {
92
- errorFormatter(viewModelSafeParse.error.issues).forEach((error) => {
93
- errors.push(`${filePath}: ${error}`);
94
- });
78
+ if (cube.views && !viewModelSafeParse.success) {
79
+ errorFormatter(viewModelSafeParse.error.issues).forEach((error) => {
80
+ errors.push(`${filePath}: ${error}`);
81
+ });
82
+ }
83
+ } catch (e: any) {
84
+ errors.push(`${filePath}: ${e.message}`);
95
85
  }
96
86
  }
97
87
 
98
88
  return errors;
99
89
  }
100
90
 
101
- async function securityContextValidation(filesList: [string, string][]) {
91
+ export async function securityContextValidation(filesList: [string, string][]) {
102
92
  const errors: string[] = [];
103
93
 
104
94
  const nameSet = new Set<string>();