@geode/opengeodeweb-front 10.30.0-rc.8 → 10.30.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.
Files changed (43) hide show
  1. package/app/components/ObjectSelector.vue +26 -76
  2. package/app/components/Viewer/Generic/Model/BlocksOptions.vue +2 -2
  3. package/app/components/Viewer/Generic/Model/CornersOptions.vue +2 -2
  4. package/app/components/Viewer/Generic/Model/LinesOptions.vue +2 -2
  5. package/app/components/Viewer/Generic/Model/SurfacesOptions.vue +2 -2
  6. package/app/components/Viewer/ObjectTree/Base/CommonTreeView.vue +1 -0
  7. package/app/components/Viewer/ObjectTree/Base/TreeRow.vue +8 -0
  8. package/app/components/Viewer/ObjectTree/Views/GlobalObjects.vue +3 -0
  9. package/app/components/Viewer/Options/AttributeSelector.vue +13 -6
  10. package/app/components/Viewer/Options/ColoringTypeSelector.vue +7 -7
  11. package/app/composables/project_manager.js +14 -11
  12. package/app/stores/app.js +8 -7
  13. package/app/stores/back.js +2 -13
  14. package/app/stores/cloud.js +3 -0
  15. package/app/stores/infra.js +0 -5
  16. package/app/stores/viewer.js +0 -12
  17. package/app/utils/extension.js +4 -3
  18. package/app/utils/import_workflow.js +1 -9
  19. package/app/utils/log.js +18 -0
  20. package/internal/utils/api_fetch.js +26 -53
  21. package/internal/utils/upload_file.js +33 -29
  22. package/internal/utils/viewer_call.js +7 -2
  23. package/package.json +17 -16
  24. package/server/api/cloud/extensions/run.post.js +72 -0
  25. package/server/api/{microservice → local}/extensions/run.post.js +10 -46
  26. package/server/api/microservice/app/set_back_base_url.post.js +22 -0
  27. package/server/api/microservice/app/set_viewer_base_url.post.js +22 -0
  28. package/server/utils/cloud.js +107 -1
  29. package/server/utils/extension.js +50 -0
  30. package/server/utils/microservices.js +51 -26
  31. package/server/utils/path.js +10 -0
  32. package/server/utils/server_config.js +4 -10
  33. package/shared/scripts.js +47 -9
  34. package/shared/utils/fetch_raw.js +1 -0
  35. package/shared/utils/response_handlers/load.js +74 -0
  36. package/tests/unit/composables/project_manager.nuxt.test.js +12 -1
  37. package/tests/unit/composables/upload_file.nuxt.test.js +2 -2
  38. package/tests/unit/stores/cloud.nuxt.test.js +23 -26
  39. package/tests/unit/stores/infra.nuxt.test.js +12 -5
  40. package/tests/unit/utils/validate_schema.nuxt.test.js +1 -1
  41. package/app/utils/validate_schema.js +0 -13
  42. /package/server/api/{microservice → local}/extensions/kill.post.js +0 -0
  43. /package/server/api/serverless/{run_cloud.js → run_cloud.post.js} +0 -0
@@ -7,6 +7,7 @@ import path from "node:path";
7
7
  import back_schemas from "@geode/opengeodeweb-back/opengeodeweb_back_schemas.json" with { type: "json" };
8
8
 
9
9
  // Local imports
10
+ import { addNginxLocation, addSupervisorProgram } from "./cloud.js";
10
11
  import { getAvailablePort, waitForReady } from "./scripts.js";
11
12
  import { microservicesMetadatasPath, projectMicroservices } from "./cleanup.js";
12
13
  import { executablePath } from "./path.js";
@@ -57,33 +58,11 @@ function isPortInUseError(errorMessage) {
57
58
  }
58
59
 
59
60
  async function runBack(execName, execPath, args = {}, attempts = 0) {
60
- const { projectFolderPath } = args;
61
- if (!projectFolderPath) {
62
- throw new Error("projectFolderPath is required");
63
- }
64
- let { uploadFolderPath } = args;
65
- if (!uploadFolderPath) {
66
- uploadFolderPath = path.join(projectFolderPath, "uploads");
67
- }
68
61
  try {
69
62
  const port = await getAvailablePort();
70
- const backArgs = [
71
- "--port",
72
- String(port),
73
- "--project_folder_path",
74
- projectFolderPath,
75
- "--upload_folder_path",
76
- uploadFolderPath,
77
- "--allowed_origins",
78
- "http://localhost:*",
79
- "--timeout",
80
- "0",
81
- ];
82
- if (process.env.NODE_ENV === "development" || !process.env.NODE_ENV) {
83
- backArgs.push("--debug");
84
- }
85
- console.log("runBack", execPath, execName, backArgs);
86
- await runScript(execPath, execName, backArgs, "Serving Flask app");
63
+ const executableArgs = backArgs(args, port);
64
+ console.log("runBack", execPath, execName, executableArgs);
65
+ await runScript(execPath, execName, executableArgs, "Serving Flask app");
87
66
  return port;
88
67
  } catch (error) {
89
68
  if (!isPortInUseError(error)) {
@@ -129,6 +108,52 @@ async function runViewer(execName, execPath, args = {}, attempts = 0) {
129
108
  }
130
109
  }
131
110
 
111
+ function backArgs(args, port) {
112
+ const { projectFolderPath } = args;
113
+ if (!projectFolderPath) {
114
+ throw new Error("projectFolderPath is required");
115
+ }
116
+ const uploadFolderPath = args.uploadFolderPath || path.join(projectFolderPath, "uploads");
117
+ const executableArgs = [
118
+ "--port",
119
+ String(port),
120
+ "--project_folder_path",
121
+ projectFolderPath,
122
+ "--upload_folder_path",
123
+ uploadFolderPath,
124
+ "--allowed_origins",
125
+ "http://localhost:*",
126
+ "--timeout",
127
+ "0",
128
+ ];
129
+ if (process.env.NODE_ENV === "development" || !process.env.NODE_ENV) {
130
+ executableArgs.push("--debug");
131
+ }
132
+ return executableArgs;
133
+ }
134
+
135
+ async function runExtension(extensionId, execName, execPath, args = {}, attempts = 0) {
136
+ try {
137
+ const port = await getAvailablePort();
138
+ const executableArgs = backArgs(args, port);
139
+ const command = executablePath(execPath, execName);
140
+ console.log("runExtension", execPath, execName, executableArgs);
141
+ addSupervisorProgram(extensionId, command, executableArgs);
142
+ addNginxLocation(extensionId, port);
143
+ return port;
144
+ } catch (error) {
145
+ if (!isPortInUseError(error)) {
146
+ console.log("runBack error", error);
147
+ throw error;
148
+ }
149
+ if (attempts <= MAX_PORT_RETRIES) {
150
+ console.log("Retrying runExtension on conflicting port", port);
151
+ const port = await runExtension(extensionId, execName, execPath, args, attempts + 1);
152
+ return port;
153
+ }
154
+ }
155
+ }
156
+
132
157
  function addMicroserviceMetadatas(projectFolderPath, serviceObj) {
133
158
  const microservices = projectMicroservices(projectFolderPath);
134
159
  if (serviceObj.type === "back") {
@@ -147,4 +172,4 @@ function addMicroserviceMetadatas(projectFolderPath, serviceObj) {
147
172
  );
148
173
  }
149
174
 
150
- export { addMicroserviceMetadatas, runBack, runViewer };
175
+ export { addMicroserviceMetadatas, runBack, runExtension, runViewer };
@@ -138,8 +138,18 @@ async function extensionFrontendPath(unzippedExtensionPath, frontendFile, rootPa
138
138
  throw new Error(`Failed to find ${unzippedfrontendFilePath}`);
139
139
  }
140
140
 
141
+ function extensionBackendPath(unzippedExtensionPath, backendExecutableName) {
142
+ const backendExecutablePath = path.join(
143
+ unzippedExtensionPath,
144
+ executableName(backendExecutableName),
145
+ );
146
+ console.log("runExtensions", { backendExecutablePath });
147
+ return backendExecutablePath;
148
+ }
149
+
141
150
  export {
142
151
  createPath,
152
+ extensionBackendPath,
143
153
  extensionFrontendPath,
144
154
  extensionFolderPath,
145
155
  executablePath,
@@ -1,30 +1,24 @@
1
- import { useStorage } from "#imports";
1
+ import { createStorage, prefixStorage } from "unstorage";
2
+
3
+ const storage = createStorage();
4
+ const config = prefixStorage(storage, "config");
2
5
 
3
- function getConfig() {
4
- return useStorage("config");
5
- }
6
6
  function getAppBaseUrl() {
7
- const config = getConfig();
8
7
  return config.getItem("APP_BASE_URL");
9
8
  }
10
9
  function setAppBaseUrl(baseUrl) {
11
- const config = getConfig();
12
10
  return config.setItem("APP_BASE_URL", baseUrl);
13
11
  }
14
12
  function getBackBaseUrl() {
15
- const config = getConfig();
16
13
  return config.getItem("BACK_BASE_URL");
17
14
  }
18
15
  function setBackBaseUrl(baseUrl) {
19
- const config = getConfig();
20
16
  return config.setItem("BACK_BASE_URL", baseUrl);
21
17
  }
22
18
  function getViewerBaseUrl() {
23
- const config = getConfig();
24
19
  return config.getItem("VIEWER_BASE_URL");
25
20
  }
26
21
  function setViewerBaseUrl(baseUrl) {
27
- const config = getConfig();
28
22
  return config.setItem("VIEWER_BASE_URL", baseUrl);
29
23
  }
30
24
 
package/shared/scripts.js CHANGED
@@ -3,16 +3,54 @@
3
3
  // Third party imports
4
4
 
5
5
  // Local imports
6
+ import { fetchSchema } from "./utils/fetch_schema.js";
6
7
 
7
- function setAppBaseUrl(baseUrl) {
8
- console.log(`Setting APP_BASE_URL to ${baseUrl}`);
9
- return fetch(`${baseUrl}/api/microservice/app/set_app_base_url`, {
10
- method: "POST",
11
- headers: {
12
- "Content-Type": "application/json",
8
+ function setAppBaseUrl(appBaseUrl) {
9
+ console.log("[API] setAppBaseUrl", appBaseUrl);
10
+ const schema = {
11
+ $id: "/api/microservice/app/set_app_base_url",
12
+ methods: ["POST"],
13
+ type: "object",
14
+ properties: {
15
+ baseUrl: { type: "string" },
13
16
  },
14
- body: JSON.stringify({ baseUrl }),
15
- });
17
+ required: ["baseUrl"],
18
+ additionalProperties: false,
19
+ };
20
+ const params = { baseUrl: appBaseUrl };
21
+ return fetchSchema({ schema, params, baseURL: appBaseUrl });
16
22
  }
17
23
 
18
- export { setAppBaseUrl };
24
+ function setBackBaseUrl(appBaseUrl, backBaseUrl) {
25
+ console.log("[API] setBackBaseUrl", appBaseUrl, backBaseUrl);
26
+ const schema = {
27
+ $id: "/api/microservice/app/set_back_base_url",
28
+ methods: ["POST"],
29
+ type: "object",
30
+ properties: {
31
+ baseUrl: { type: "string" },
32
+ },
33
+ required: ["baseUrl"],
34
+ additionalProperties: false,
35
+ };
36
+ const params = { baseUrl: backBaseUrl };
37
+ return fetchSchema({ schema, params, baseURL: appBaseUrl });
38
+ }
39
+
40
+ function setViewerBaseUrl(appBaseUrl, viewerBaseUrl) {
41
+ console.log("[API] setViewerBaseUrl", appBaseUrl, viewerBaseUrl);
42
+ const schema = {
43
+ $id: "/api/microservice/app/set_viewer_base_url",
44
+ methods: ["POST"],
45
+ type: "object",
46
+ properties: {
47
+ baseUrl: { type: "string" },
48
+ },
49
+ required: ["baseUrl"],
50
+ additionalProperties: false,
51
+ };
52
+ const params = { baseUrl: viewerBaseUrl };
53
+ return fetchSchema({ schema, params, baseURL: appBaseUrl });
54
+ }
55
+
56
+ export { setAppBaseUrl, setBackBaseUrl, setViewerBaseUrl };
@@ -1,4 +1,5 @@
1
1
  // Third party imports
2
+ import { $fetch } from "ofetch";
2
3
  import _ from "lodash";
3
4
  import pTimeout from "p-timeout";
4
5
 
@@ -0,0 +1,74 @@
1
+ function getFileExtension(filename) {
2
+ return filename.slice(filename.lastIndexOf(".") + 1);
3
+ }
4
+
5
+ function selectGeodeObject(objectMap) {
6
+ const objectKeys = Object.keys(objectMap);
7
+ if (objectKeys.length === 0) {
8
+ return undefined;
9
+ }
10
+ if (objectKeys.length === 1 && objectMap[objectKeys[0]].is_loadable > 0) {
11
+ return objectKeys[0];
12
+ }
13
+
14
+ const highestLoadScore = Math.max(...objectKeys.map((key) => objectMap[key].is_loadable));
15
+ if (highestLoadScore <= 0) {
16
+ return undefined;
17
+ }
18
+
19
+ const bestScoreObjects = objectKeys.filter(
20
+ (key) => objectMap[key].is_loadable === highestLoadScore,
21
+ );
22
+ if (bestScoreObjects.length === 1) {
23
+ return bestScoreObjects[0];
24
+ }
25
+
26
+ const highestPriority = Math.max(
27
+ ...bestScoreObjects.map((key) => objectMap[key].object_priority ?? -Infinity),
28
+ );
29
+ const bestPriorityObjects = bestScoreObjects.filter(
30
+ (key) => objectMap[key].object_priority === highestPriority,
31
+ );
32
+ if (highestPriority !== -Infinity && bestPriorityObjects.length === 1) {
33
+ return bestPriorityObjects[0];
34
+ }
35
+
36
+ return undefined;
37
+ }
38
+
39
+ function intersectAllowedObjects(allowedObjectsList) {
40
+ const allKeys = [...new Set(allowedObjectsList.flatMap((object) => Object.keys(object)))];
41
+ const commonKeys = allKeys.filter((key) => allowedObjectsList.every((object) => key in object));
42
+
43
+ const mergedAllowedObjects = {};
44
+ for (const key of commonKeys) {
45
+ const loadScores = allowedObjectsList.map((object) => object[key].is_loadable);
46
+ const priorities = allowedObjectsList
47
+ .map((object) => object[key].object_priority)
48
+ .filter((priority) => priority !== undefined);
49
+
50
+ mergedAllowedObjects[key] = { is_loadable: Math.min(...loadScores) };
51
+ if (priorities.length > 0) {
52
+ mergedAllowedObjects[key].object_priority = Math.max(...priorities);
53
+ }
54
+ }
55
+
56
+ return { commonKeys, allKeys, mergedAllowedObjects };
57
+ }
58
+
59
+ function resolveAllowedObjects(filenames, allowedObjectsList) {
60
+ const { commonKeys, allKeys, mergedAllowedObjects } = intersectAllowedObjects(allowedObjectsList);
61
+
62
+ const multipleFilesNoCommon =
63
+ filenames.length > 1 && allKeys.length > 0 && commonKeys.length === 0;
64
+
65
+ const selectedGeodeObject = selectGeodeObject(mergedAllowedObjects);
66
+
67
+ return {
68
+ mergedAllowedObjects,
69
+ multipleFilesNoCommon,
70
+ selectedGeodeObject,
71
+ };
72
+ }
73
+
74
+ export { resolveAllowedObjects, getFileExtension };
@@ -8,6 +8,12 @@ import { exportProject, importProject } from "@ogw_front/composables/project_man
8
8
  import { appMode } from "@ogw_shared/app_mode";
9
9
  import { setupActivePinia } from "@ogw_tests/utils";
10
10
 
11
+ import { $fetch } from "ofetch";
12
+
13
+ vi.mock(import("ofetch"), () => ({
14
+ $fetch: vi.fn(),
15
+ }));
16
+
11
17
  // Constants
12
18
  const PANEL_WIDTH = 300;
13
19
  const Z_SCALE = 1.5;
@@ -141,7 +147,12 @@ const hybridViewerStoreMock = {
141
147
  };
142
148
 
143
149
  // MOCKS
144
- vi.stubGlobal("$fetch", vi.fn().mockResolvedValue({ snapshot: snapshotMock }));
150
+ $fetch.mockImplementation((route, options) => {
151
+ const data = { snapshot: snapshotMock };
152
+ // oxlint-disable-next-line eslint/id-length
153
+ options.onResponse?.({ response: { ok: true, _data: data } });
154
+ return Promise.resolve(data);
155
+ });
145
156
  vi.mock(import("@ogw_internal/utils/viewer_call"), () => ({
146
157
  viewer_call: viewer_call_mock_fn,
147
158
  }));
@@ -22,7 +22,7 @@ describe("upload_file", () => {
22
22
  const backStore = useBackStore();
23
23
  const file = "toto";
24
24
 
25
- await expect(backStore.upload(file)).rejects.toThrow("file must be a instance of File");
25
+ await expect(backStore.upload(file)).rejects.toThrow("file must be an instance of File");
26
26
  });
27
27
 
28
28
  test("onResponse", async () => {
@@ -36,7 +36,7 @@ describe("upload_file", () => {
36
36
  let response_value = "";
37
37
  await backStore.upload(file, {
38
38
  response_function: (response) => {
39
- response_value = response._data.test;
39
+ response_value = response.test;
40
40
  },
41
41
  });
42
42
  expect(feedbackStore.feedbacks).toHaveLength(ZERO);
@@ -1,6 +1,6 @@
1
1
  // Third party imports
2
2
  import { beforeEach, describe, expect, expectTypeOf, test, vi } from "vitest";
3
- import { registerEndpoint } from "@nuxt/test-utils/runtime";
3
+ import { $fetch } from "ofetch";
4
4
 
5
5
  // Local imports
6
6
  import { Status } from "@ogw_front/utils/status";
@@ -8,9 +8,12 @@ import { setupActivePinia } from "@ogw_tests/utils";
8
8
  import { useCloudStore } from "@ogw_front/stores/cloud";
9
9
  import { useFeedbackStore } from "@ogw_front/stores/feedback";
10
10
 
11
+ vi.mock(import("ofetch"), () => ({
12
+ $fetch: vi.fn(),
13
+ }));
14
+
11
15
  // CONSTANTS
12
16
  const PROJECT = "project";
13
- const STATUS_500 = 500;
14
17
 
15
18
  function setupConfig() {
16
19
  const config = useRuntimeConfig();
@@ -31,28 +34,23 @@ describe("cloud store", () => {
31
34
 
32
35
  describe("actions", () => {
33
36
  describe("launch", () => {
34
- const postFakeCall = vi.fn();
37
+ beforeEach(() => {
38
+ $fetch.mockReset();
39
+ });
35
40
 
36
41
  test("successful launch", async () => {
37
42
  setupConfig();
38
43
  const cloudStore = useCloudStore();
39
44
  const feedbackStore = useFeedbackStore();
40
45
 
41
- registerEndpoint("https://localhost:443/server/api/serverless/run_cloud", {
42
- method: "POST",
43
- handler: postFakeCall,
46
+ $fetch.mockImplementation((route, options) => {
47
+ const data = { url: "test.com" };
48
+ // oxlint-disable-next-line eslint/id-length
49
+ options.onResponse?.({ response: { ok: true, _data: data } });
50
+ return Promise.resolve(data);
44
51
  });
45
52
 
46
- registerEndpoint("https://test.com:443/server/api/microservice/app/set_app_base_url", {
47
- method: "POST",
48
- handler: () => console.log("coucou from endpoint"),
49
- });
50
-
51
- postFakeCall.mockReturnValue({
52
- url: "test.com",
53
- });
54
- const email = "noreply@example.com";
55
- await cloudStore.launch(email);
53
+ await cloudStore.launch("noreply@example.com");
56
54
 
57
55
  expect(cloudStore.status).toBe(Status.CONNECTED);
58
56
  expect(feedbackStore.server_error).toBe(false);
@@ -63,19 +61,18 @@ describe("cloud store", () => {
63
61
  const cloudStore = useCloudStore();
64
62
  const feedbackStore = useFeedbackStore();
65
63
 
66
- registerEndpoint("https://localhost:443/server/api/serverless/run_cloud", {
67
- method: "POST",
68
- handler: postFakeCall,
69
- });
64
+ const error = createError({ statusCode: 500, statusMessage: "500 Internal Server Error" });
70
65
 
71
- postFakeCall.mockImplementation(() => {
72
- throw createError({
73
- status: STATUS_500,
74
- statusMessage: "Internal Server Error",
66
+ $fetch.mockImplementation((route, options) => {
67
+ options.onResponseError?.({
68
+ response: { status: 500, name: "Error", description: "500 Internal Server Error" },
75
69
  });
70
+ return Promise.reject(error);
76
71
  });
77
- const email = "noreply@example.com";
78
- await expect(cloudStore.launch(email)).rejects.toThrow("500 Internal Server Error");
72
+
73
+ await expect(cloudStore.launch("noreply@example.com")).rejects.toThrow(
74
+ "500 Internal Server Error",
75
+ );
79
76
 
80
77
  expect(cloudStore.status).toBe(Status.NOT_CONNECTED);
81
78
  expect(feedbackStore.server_error).toBe(true);
@@ -1,6 +1,6 @@
1
1
  // Third party imports
2
2
  import { beforeEach, describe, expect, expectTypeOf, test, vi } from "vitest";
3
- import { registerEndpoint } from "@nuxt/test-utils/runtime";
3
+ import { $fetch } from "ofetch";
4
4
 
5
5
  // Local imports
6
6
  import { Status } from "@ogw_front/utils/status";
@@ -10,6 +10,10 @@ import { useBackStore } from "@ogw_front/stores/back";
10
10
  import { useInfraStore } from "@ogw_front/stores/infra";
11
11
  import { useViewerStore } from "@ogw_front/stores/viewer";
12
12
 
13
+ vi.mock(import("ofetch"), () => ({
14
+ $fetch: vi.fn(),
15
+ }));
16
+
13
17
  // Mock navigator.locks API
14
18
  const mockLockRequest = vi
15
19
  .fn()
@@ -291,11 +295,14 @@ describe("infra store", () => {
291
295
 
292
296
  infraStore.app_mode = appMode.CLOUD;
293
297
  const url = "test.com";
294
- registerEndpoint("https://localhost:443/server/api/serverless/run_cloud", {
295
- method: "POST",
296
- handler: () => ({ url }),
298
+ $fetch.mockImplementation((route, options) => {
299
+ const data = { url };
300
+ // oxlint-disable-next-line eslint/id-length
301
+ options.onResponse?.({ response: { ok: true, _data: data } });
302
+ return Promise.resolve(data);
297
303
  });
298
- await infraStore.create_backend("", "", false);
304
+
305
+ await infraStore.create_backend("noreply@example.com");
299
306
  expect(infraStore.status).toBe(Status.CREATED);
300
307
  expect(infraStore.domain_name).toBe(url);
301
308
 
@@ -2,7 +2,7 @@
2
2
  import { describe, expect, test } from "vitest";
3
3
 
4
4
  // Local imports
5
- import { validate_schema } from "@ogw_front/utils/validate_schema";
5
+ import { validate_schema } from "@ogw_shared/utils/validate_schema";
6
6
 
7
7
  // CONSTANTS
8
8
  const MIN_0 = 0;
@@ -1,13 +0,0 @@
1
- import Ajv from "ajv";
2
-
3
- function validate_schema(schema, body) {
4
- const ajv = new Ajv();
5
- const list_keywords = ["methods", "route", "max_retry", "rpc"];
6
- for (const keyword of list_keywords) {
7
- ajv.addKeyword(keyword);
8
- }
9
- const valid = ajv.validate(schema, body);
10
- return { valid, error: ajv.errorsText() };
11
- }
12
-
13
- export { validate_schema };