@geode/opengeodeweb-front 10.30.0-rc.15 → 10.30.0-rc.16

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.
@@ -4,6 +4,7 @@
4
4
  import _ from "lodash";
5
5
 
6
6
  // Local imports
7
+ import { isCloudMode } from "@ogw_front/utils/stores";
7
8
  import { useAppStore } from "@ogw_front/stores/app";
8
9
  import { useInfraStore } from "@ogw_front/stores/infra";
9
10
 
@@ -105,9 +106,9 @@ function runExtensions() {
105
106
  const { projectFolderPath } = appStore;
106
107
  const { PROJECT: projectName } = useRuntimeConfig().public;
107
108
  const params = { projectFolderPath, projectName };
108
-
109
+ const endpoint = isCloudMode() ? "cloud" : "local";
109
110
  const schema = {
110
- $id: "/api/microservice/extensions/run",
111
+ $id: `/api/${endpoint}/extensions/run`,
111
112
  methods: ["POST"],
112
113
  type: "object",
113
114
  properties: {
@@ -130,7 +131,7 @@ function killExtension(extensionId) {
130
131
  console.log(`[AppStore] Killing extension: ${extensionId}`, { params });
131
132
 
132
133
  const schema = {
133
- $id: "/api/microservice/extensions/kill",
134
+ $id: "/api/local/extensions/kill",
134
135
  methods: ["POST"],
135
136
  type: "object",
136
137
  properties: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geode/opengeodeweb-front",
3
- "version": "10.30.0-rc.15",
3
+ "version": "10.30.0-rc.16",
4
4
  "description": "OpenSource Vue/Nuxt/Pinia/Vuetify framework for web applications",
5
5
  "homepage": "https://github.com/Geode-solutions/OpenGeodeWeb-Front",
6
6
  "bugs": {
@@ -0,0 +1,72 @@
1
+ // Node imports
2
+ import fs from "node:fs";
3
+
4
+ // Third party imports
5
+ import { createError, defineEventHandler, readBody } from "h3";
6
+
7
+ // Local imports
8
+ import {
9
+ addMicroserviceMetadatas,
10
+ runExtension,
11
+ } from "@geode/opengeodeweb-front/server/utils/microservices.js";
12
+ import {
13
+ extensionBackendPath,
14
+ extensionFolderPath,
15
+ } from "@geode/opengeodeweb-front/server/utils/path.js";
16
+ import {
17
+ readExtensionFrontend,
18
+ readExtensionMetadata,
19
+ } from "@geode/opengeodeweb-front/server/utils/extension.js";
20
+ import { extensionsConf } from "@geode/opengeodeweb-front/server/utils/app_config.js";
21
+ import { unzipFile } from "@geode/opengeodeweb-front/server/utils/server.js";
22
+
23
+ export default defineEventHandler(async (event) => {
24
+ try {
25
+ console.log("NITRO: runExtensions", event);
26
+ const { projectFolderPath, projectName } = await readBody(event);
27
+ const extensionsConfig = extensionsConf(projectName);
28
+ const extensionsArray = await Promise.all(
29
+ Object.keys(extensionsConfig).map(async (extensionId) => {
30
+ const extensionPath = extensionsConfig[extensionId].path;
31
+ const unzippedExtensionPath = await unzipFile(
32
+ extensionPath,
33
+ extensionFolderPath(projectFolderPath, extensionId),
34
+ );
35
+ const { id, name, version, backendExecutable, frontendFile } =
36
+ await readExtensionMetadata(unzippedExtensionPath);
37
+ const frontendContent = await readExtensionFrontend(
38
+ unzippedExtensionPath,
39
+ frontendFile,
40
+ id,
41
+ );
42
+ fs.chmodSync(extensionBackendPath(unzippedExtensionPath, backendExecutable), "755");
43
+ const port = await runExtension(id, backendExecutable, unzippedExtensionPath, {
44
+ projectFolderPath,
45
+ });
46
+ await addMicroserviceMetadatas(projectFolderPath, {
47
+ type: "back",
48
+ name,
49
+ port,
50
+ });
51
+ return {
52
+ id,
53
+ name,
54
+ version,
55
+ frontendContent,
56
+ port,
57
+ };
58
+ }),
59
+ );
60
+
61
+ return {
62
+ statusCode: 200,
63
+ extensionsArray,
64
+ };
65
+ } catch (error) {
66
+ console.error("Error running extensions:", error);
67
+ throw createError({
68
+ statusCode: 500,
69
+ statusMessage: error.message,
70
+ });
71
+ }
72
+ });
@@ -1,6 +1,5 @@
1
1
  // Node imports
2
2
  import fs from "node:fs";
3
- import path from "node:path";
4
3
 
5
4
  // Third party imports
6
5
  import { createError, defineEventHandler, readBody } from "h3";
@@ -11,20 +10,21 @@ import {
11
10
  runBack,
12
11
  } from "@geode/opengeodeweb-front/server/utils/microservices.js";
13
12
  import {
14
- executableName,
13
+ extensionBackendPath,
15
14
  extensionFolderPath,
16
- extensionFrontendPath,
17
15
  } from "@geode/opengeodeweb-front/server/utils/path.js";
16
+ import {
17
+ readExtensionFrontend,
18
+ readExtensionMetadata,
19
+ } from "@geode/opengeodeweb-front/server/utils/extension.js";
18
20
  import { extensionsConf } from "@geode/opengeodeweb-front/server/utils/app_config.js";
19
21
  import { unzipFile } from "@geode/opengeodeweb-front/server/utils/server.js";
20
22
 
21
23
  export default defineEventHandler(async (event) => {
22
24
  try {
23
25
  console.log("NITRO: runExtensions", event);
24
- const body = await readBody(event);
25
- const { projectFolderPath, projectName } = body;
26
+ const { projectFolderPath, projectName } = await readBody(event);
26
27
  const extensionsConfig = extensionsConf(projectName);
27
-
28
28
  const extensionsArray = await Promise.all(
29
29
  Object.keys(extensionsConfig).map(async (extensionId) => {
30
30
  const extensionPath = extensionsConfig[extensionId].path;
@@ -32,50 +32,14 @@ export default defineEventHandler(async (event) => {
32
32
  extensionPath,
33
33
  extensionFolderPath(projectFolderPath, extensionId),
34
34
  );
35
- const metadataPath = path.join(unzippedExtensionPath, "metadata.json");
36
- const metadataContent = await fs.promises.readFile(metadataPath, "utf8");
37
-
38
- if (!metadataContent) {
39
- throw createError({
40
- statusCode: 400,
41
- statusMessage: "Invalid extension file: missing metadata.json",
42
- });
43
- }
44
- const metadata = JSON.parse(metadataContent);
45
- console.log("runExtensions", { metadata });
46
-
47
- const { id, name, version, backendExecutable, frontendFile } = metadata;
48
- console.log("runExtensions", { id, name, version, backendExecutable });
49
-
50
- if (!frontendFile) {
51
- throw createError({
52
- statusCode: 400,
53
- statusMessage: "Invalid extension file: missing frontend JavaScript",
54
- });
55
- }
56
- if (!backendExecutable) {
57
- throw createError({
58
- statusCode: 400,
59
- statusMessage: "Invalid extension file: missing backend executable",
60
- });
61
- }
62
-
63
- const frontendFilePath = await extensionFrontendPath(
35
+ const { id, name, version, backendExecutable, frontendFile } =
36
+ await readExtensionMetadata(unzippedExtensionPath);
37
+ const frontendContent = await readExtensionFrontend(
64
38
  unzippedExtensionPath,
65
39
  frontendFile,
66
- path.resolve(),
67
40
  id,
68
41
  );
69
-
70
- console.log("runExtensions", { frontendFilePath });
71
- const frontendContent = await fs.promises.readFile(frontendFilePath, "utf8");
72
-
73
- const backendExecutablePath = path.join(
74
- unzippedExtensionPath,
75
- executableName(backendExecutable),
76
- );
77
- console.log("runExtensions", { backendExecutablePath });
78
- fs.chmodSync(backendExecutablePath, "755");
42
+ fs.chmodSync(extensionBackendPath(unzippedExtensionPath, backendExecutable), "755");
79
43
  const port = await runBack(backendExecutable, unzippedExtensionPath, {
80
44
  projectFolderPath,
81
45
  });
@@ -5,6 +5,8 @@ import { google } from "googleapis";
5
5
 
6
6
  // Local imports
7
7
 
8
+ const LOCATIONS_DIR = "/etc/nginx/locations";
9
+
8
10
  async function artifactImage(parent, authClient) {
9
11
  const projectName = process.env.PROJECT;
10
12
  const registry = google.artifactregistry({
@@ -92,4 +94,108 @@ function requestConfig(parent, image, email, projectName) {
92
94
  };
93
95
  }
94
96
 
95
- export { artifactImage, requestConfig };
97
+ function addSupervisorProgram(name, command, executableArgs) {
98
+ const conf = `
99
+ [program:${name}]
100
+ command=${command} ${executableArgs.join(" ")}
101
+ autostart=true
102
+ autorestart=true
103
+ stdout_logfile=/dev/stdout
104
+ stdout_logfile_maxbytes=0
105
+ stderr_logfile=/dev/stderr
106
+ stderr_logfile_maxbytes=0
107
+ `;
108
+ const confPath = path.join("/etc/supervisor/conf.d", `${name}.conf`);
109
+ fs.writeFileSync(confPath, conf);
110
+ execFileSync("supervisorctl", ["reread"]);
111
+ execFileSync("supervisorctl", ["update"]);
112
+ const stdout = execFileSync("supervisorctl", ["start", name]);
113
+ console.log("addSupervisorProgram", stdout);
114
+ }
115
+
116
+ function buildLocationBlock(routePath, port) {
117
+ if (!routePath.startsWith("/") || !routePath.endsWith("/")) {
118
+ throw new Error(`routePath must start and end with '/', got: ${routePath}`);
119
+ }
120
+ const methods = "GET, POST, PUT, DELETE, PATCH, OPTIONS";
121
+ const limitMethods = methods
122
+ .split(",")
123
+ .map((method) => method.trim())
124
+ .filter((method) => method !== "OPTIONS")
125
+ .join(" ");
126
+
127
+ return `# ====================== ${routePath} location ======================
128
+ location ~ "^${routePath}" {
129
+ if ($request_method = 'OPTIONS') {
130
+ add_header 'Access-Control-Allow-Origin' $allow_origin always;
131
+ add_header 'Access-Control-Allow-Credentials' 'true' always;
132
+ add_header 'Access-Control-Allow-Methods' '${methods}' always;
133
+ add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,X-CSRF-Token' always;
134
+ add_header 'Access-Control-Max-Age' 1728000 always;
135
+ add_header 'Content-Type' 'text/plain; charset=utf-8';
136
+ add_header 'Content-Length' 0;
137
+ return 204;
138
+ }
139
+
140
+ limit_except ${limitMethods} { deny all; }
141
+
142
+ add_header 'Access-Control-Allow-Origin' $allow_origin always;
143
+ add_header 'Access-Control-Allow-Credentials' 'true' always;
144
+ add_header 'Access-Control-Allow-Methods' '${methods}' always;
145
+ add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,X-CSRF-Token' always;
146
+ add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;
147
+ add_header 'Vary' 'Origin' always;
148
+
149
+ rewrite "^${routePath}(.*)" /$1 break;
150
+ proxy_pass http://localhost:${port};
151
+ proxy_http_version 1.1;
152
+ proxy_set_header Host $host;
153
+ proxy_set_header X-Real-IP $remote_addr;
154
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
155
+ proxy_set_header X-Forwarded-Proto $scheme;
156
+ }
157
+ `;
158
+ }
159
+
160
+ function nginxConfigFile(name) {
161
+ return path.join(LOCATIONS_DIR, `${name}.conf`);
162
+ }
163
+
164
+ function nginxReload() {
165
+ execFileSync("nginx", ["-t"]);
166
+ execFileSync("nginx", ["-s", "reload"]);
167
+ }
168
+
169
+ function addNginxLocation(name, port) {
170
+ fs.mkdirSync(LOCATIONS_DIR, { recursive: true });
171
+ const filePath = nginxConfigFile(name);
172
+ if (fs.existsSync(filePath)) {
173
+ throw new Error(`Location '${name}' already exists at ${filePath}`);
174
+ }
175
+ fs.writeFileSync(filePath, buildLocationBlock(`/${name}/`, port));
176
+ try {
177
+ nginxReload();
178
+ } catch (error) {
179
+ fs.unlinkSync(filePath);
180
+ throw error;
181
+ }
182
+ return filePath;
183
+ }
184
+
185
+ function removeNginxLocation(name) {
186
+ const filePath = filePathFor(name);
187
+ if (!fs.existsSync(filePath)) {
188
+ return false;
189
+ }
190
+ fs.unlinkSync(filePath);
191
+ nginxReload();
192
+ return true;
193
+ }
194
+
195
+ export {
196
+ addNginxLocation,
197
+ addSupervisorProgram,
198
+ artifactImage,
199
+ removeNginxLocation,
200
+ requestConfig,
201
+ };
@@ -0,0 +1,50 @@
1
+ // Node imports
2
+ import { promises as fs } from "node:fs";
3
+ import path from "node:path";
4
+
5
+ // Third party imports
6
+ import { createError } from "h3";
7
+
8
+ // Local imports
9
+ import { extensionFrontendPath } from "@geode/opengeodeweb-front/server/utils/path.js";
10
+
11
+ async function readExtensionMetadata(unzippedExtensionPath) {
12
+ const metadataPath = path.join(unzippedExtensionPath, "metadata.json");
13
+ const metadataContent = await fs.readFile(metadataPath, "utf8");
14
+ if (!metadataContent) {
15
+ throw createError({
16
+ statusCode: 400,
17
+ statusMessage: "Invalid extension file: missing metadata.json",
18
+ });
19
+ }
20
+ const metadata = JSON.parse(metadataContent);
21
+ console.log("readExtensionMetadata", { metadata });
22
+ if (!metadata.frontendFile) {
23
+ throw createError({
24
+ statusCode: 400,
25
+ statusMessage: "Invalid extension file: missing frontend JavaScript",
26
+ });
27
+ }
28
+ if (!metadata.backendExecutable) {
29
+ throw createError({
30
+ statusCode: 400,
31
+ statusMessage: "Invalid extension file: missing backend executable",
32
+ });
33
+ }
34
+
35
+ return metadata;
36
+ }
37
+
38
+ async function readExtensionFrontend(unzippedExtensionPath, frontendFile, id) {
39
+ console.log("readExtensionFrontend", { id });
40
+ const frontendFilePath = await extensionFrontendPath(
41
+ unzippedExtensionPath,
42
+ frontendFile,
43
+ path.resolve(),
44
+ id,
45
+ );
46
+ console.log("readExtensionFrontend", { frontendFilePath });
47
+ return fs.readFile(frontendFilePath, "utf8");
48
+ }
49
+
50
+ export { readExtensionFrontend, readExtensionMetadata };
@@ -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,