@geode/opengeodeweb-front 10.30.0-rc.14 → 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.
@@ -238,7 +238,7 @@ const polyhedronSchema = back_schemas.opengeodeweb_back.model_component_polyhedr
238
238
  <VisibilitySwitch data-testid="modelBlocksVisibilitySwitch" v-model="blocksVisibility" />
239
239
  <ViewerOptionsColoringTypeSelector
240
240
  :id="modelId"
241
- :componentId="targetBlockIds[0]"
241
+ :componentIds="targetBlockIds"
242
242
  v-model:coloring_style_key="blocksActiveColoring"
243
243
  v-model:color="blocksColor"
244
244
  v-model:vertex_attribute_name="blocksVertexAttributeName"
@@ -259,7 +259,7 @@ const polyhedronSchema = back_schemas.opengeodeweb_back.model_component_polyhedr
259
259
  <VisibilitySwitch v-model="blockVisibility" />
260
260
  <ViewerOptionsColoringTypeSelector
261
261
  :id="modelId"
262
- :componentId="blockId"
262
+ :componentIds="[blockId]"
263
263
  v-model:coloring_style_key="blockActiveColoring"
264
264
  v-model:color="blockColor"
265
265
  v-model:vertex_attribute_name="vertexAttributeName"
@@ -159,7 +159,7 @@ const vertexSchema = back_schemas.opengeodeweb_back.model_component_vertex_attri
159
159
  <VisibilitySwitch data-testid="modelCornersVisibilitySwitch" v-model="cornersVisibility" />
160
160
  <ViewerOptionsColoringTypeSelector
161
161
  :id="modelId"
162
- :componentId="targetCornerIds[0]"
162
+ :componentIds="targetCornerIds"
163
163
  v-model:coloring_style_key="cornersActiveColoring"
164
164
  v-model:color="cornersColor"
165
165
  v-model:vertex_attribute_name="cornersVertexAttributeName"
@@ -176,7 +176,7 @@ const vertexSchema = back_schemas.opengeodeweb_back.model_component_vertex_attri
176
176
  <VisibilitySwitch v-model="cornerVisibility" />
177
177
  <ViewerOptionsColoringTypeSelector
178
178
  :id="modelId"
179
- :componentId="cornerId"
179
+ :componentIds="[cornerId]"
180
180
  v-model:coloring_style_key="cornerActiveColoring"
181
181
  v-model:color="cornerColor"
182
182
  v-model:vertex_attribute_name="vertexAttributeName"
@@ -234,7 +234,7 @@ const edgeSchema = back_schemas.opengeodeweb_back.model_component_edge_attribute
234
234
  <VisibilitySwitch data-testid="modelLinesVisibilitySwitch" v-model="linesVisibility" />
235
235
  <ViewerOptionsColoringTypeSelector
236
236
  :id="modelId"
237
- :componentId="targetLineIds[0]"
237
+ :componentIds="targetLineIds"
238
238
  v-model:coloring_style_key="linesActiveColoring"
239
239
  v-model:color="linesColor"
240
240
  v-model:vertex_attribute_name="linesVertexAttributeName"
@@ -255,7 +255,7 @@ const edgeSchema = back_schemas.opengeodeweb_back.model_component_edge_attribute
255
255
  <VisibilitySwitch v-model="lineVisibility" />
256
256
  <ViewerOptionsColoringTypeSelector
257
257
  :id="modelId"
258
- :componentId="lineId"
258
+ :componentIds="[lineId]"
259
259
  v-model:coloring_style_key="lineActiveColoring"
260
260
  v-model:color="lineColor"
261
261
  v-model:vertex_attribute_name="vertexAttributeName"
@@ -242,7 +242,7 @@ const polygonSchema = back_schemas.opengeodeweb_back.model_component_polygon_att
242
242
  <VisibilitySwitch data-testid="modelSurfacesVisibilitySwitch" v-model="surfacesVisibility" />
243
243
  <ViewerOptionsColoringTypeSelector
244
244
  :id="modelId"
245
- :componentId="targetSurfaceIds[0]"
245
+ :componentIds="targetSurfaceIds"
246
246
  v-model:coloring_style_key="surfacesActiveColoring"
247
247
  v-model:color="surfacesColor"
248
248
  v-model:vertex_attribute_name="surfacesVertexAttributeName"
@@ -263,7 +263,7 @@ const polygonSchema = back_schemas.opengeodeweb_back.model_component_polygon_att
263
263
  <VisibilitySwitch v-model="surfaceVisibility" />
264
264
  <ViewerOptionsColoringTypeSelector
265
265
  :id="modelId"
266
- :componentId="surfaceId"
266
+ :componentIds="[surfaceId]"
267
267
  v-model:coloring_style_key="surfaceActiveColoring"
268
268
  v-model:color="surfaceColor"
269
269
  v-model:vertex_attribute_name="vertexAttributeName"
@@ -142,6 +142,7 @@ const { focusedIndex, handleKeyDown } = useTreeKeyboardNav(
142
142
  >
143
143
  <template #default="{ item, index }">
144
144
  <v-list-item
145
+ data-testid="treeRowWrapper"
145
146
  :class="[
146
147
  'tree-row-wrapper',
147
148
  {
@@ -48,6 +48,7 @@ function handleRowClick(event) {
48
48
  <div class="d-flex align-center flex-shrink-0">
49
49
  <v-icon
50
50
  v-if="!item.isLeaf"
51
+ :data-testid="item.isOpen ? 'collapseTreeRowButton' : 'expandTreeRowButton'"
51
52
  :icon="item.isOpen ? 'mdi-menu-down' : 'mdi-menu-right'"
52
53
  class="me-1"
53
54
  color="black"
@@ -69,6 +70,13 @@ function handleRowClick(event) {
69
70
  />
70
71
  <v-btn
71
72
  v-else-if="showEyeButton"
73
+ :data-testid="
74
+ getIndeterminate(item.raw)
75
+ ? 'indeterminateObjectEyeButton'
76
+ : isSelected(item.raw)
77
+ ? 'visibleObjectEyeButton'
78
+ : 'hiddenObjectEyeButton'
79
+ "
72
80
  :icon="
73
81
  getIndeterminate(item.raw)
74
82
  ? 'mdi-eye-minus-outline'
@@ -182,6 +182,7 @@ function expandAll() {
182
182
  <template v-if="item.geode_object_type !== 'HorizonStack3D'">
183
183
  <v-btn
184
184
  v-if="item.viewer_type"
185
+ data-testid="focusObjectButton"
185
186
  icon="mdi-target"
186
187
  size="medium"
187
188
  variant="text"
@@ -190,6 +191,7 @@ function expandAll() {
190
191
  />
191
192
  <v-btn
192
193
  v-if="isModel(item)"
194
+ data-testid="expandModelComponentsButton"
193
195
  icon="mdi-magnify-expand"
194
196
  size="medium"
195
197
  class="ml-2"
@@ -206,6 +208,7 @@ function expandAll() {
206
208
  />
207
209
  <v-btn
208
210
  v-if="isModel(item) && hasCollectionsMap[item.id]"
211
+ data-testid="expandModelCollectionsButton"
209
212
  icon="mdi-format-list-group"
210
213
  size="medium"
211
214
  class="ml-2"
@@ -9,9 +9,9 @@ const attributeItem = defineModel("attributeItem", { type: Number });
9
9
  const attributeRange = defineModel("attributeRange", { type: Array });
10
10
  const attributeColorMap = defineModel("attributeColorMap", { type: String });
11
11
 
12
- const { id, componentId, schema } = defineProps({
12
+ const { id, componentIds, schema } = defineProps({
13
13
  id: { type: String, required: true },
14
- componentId: { type: String, default: undefined },
14
+ componentIds: { type: Array, default: undefined },
15
15
  schema: { type: Object, required: true },
16
16
  });
17
17
 
@@ -64,14 +64,21 @@ function resetRange() {
64
64
  }
65
65
  }
66
66
 
67
+ function hasSelectedComponent(components) {
68
+ return Array.isArray(components) && components.length > 0;
69
+ }
70
+
67
71
  function getAttributes() {
68
- if (schema.properties.component_id && componentId === undefined) {
72
+ const requiresComponent = schema.properties.component_ids !== undefined;
73
+ if (requiresComponent && !hasSelectedComponent(componentIds)) {
69
74
  return;
70
75
  }
76
+
71
77
  const params = { id };
72
- if (componentId !== undefined) {
73
- params.component_id = componentId;
78
+ if (requiresComponent) {
79
+ params.component_ids = componentIds;
74
80
  }
81
+
75
82
  backStore.request(
76
83
  { schema, params },
77
84
  {
@@ -87,7 +94,7 @@ onMounted(() => {
87
94
  });
88
95
 
89
96
  watch(
90
- () => [id, componentId, schema],
97
+ () => [id, componentIds, schema],
91
98
  () => {
92
99
  getAttributes();
93
100
  },
@@ -36,9 +36,9 @@ const polyhedron_attribute_color_map = defineModel("polyhedron_attribute_color_m
36
36
  type: String,
37
37
  });
38
38
 
39
- const { id, componentId, capabilities, schemas, allowRandom } = defineProps({
39
+ const { id, componentIds, capabilities, schemas, allowRandom } = defineProps({
40
40
  id: { type: String, required: true },
41
- componentId: { type: String, default: undefined },
41
+ componentIds: { type: Array, default: undefined },
42
42
  capabilities: {
43
43
  type: Object,
44
44
  default: () => ({}),
@@ -215,7 +215,7 @@ watch(
215
215
  v-model:attributeRange="vertex_attribute_range"
216
216
  v-model:attributeColorMap="vertex_attribute_color_map"
217
217
  :id="id"
218
- :componentId="componentId"
218
+ :componentIds="componentIds"
219
219
  :schema="vertexSchema"
220
220
  />
221
221
  </template>
@@ -226,7 +226,7 @@ watch(
226
226
  v-model:attributeRange="edge_attribute_range"
227
227
  v-model:attributeColorMap="edge_attribute_color_map"
228
228
  :id="id"
229
- :componentId="componentId"
229
+ :componentIds="componentIds"
230
230
  :schema="edgeSchema"
231
231
  />
232
232
  </template>
@@ -237,7 +237,7 @@ watch(
237
237
  v-model:attributeRange="cell_attribute_range"
238
238
  v-model:attributeColorMap="cell_attribute_color_map"
239
239
  :id="id"
240
- :componentId="componentId"
240
+ :componentIds="componentIds"
241
241
  :schema="cellSchema"
242
242
  />
243
243
  </template>
@@ -248,7 +248,7 @@ watch(
248
248
  v-model:attributeRange="polygon_attribute_range"
249
249
  v-model:attributeColorMap="polygon_attribute_color_map"
250
250
  :id="id"
251
- :componentId="componentId"
251
+ :componentIds="componentIds"
252
252
  :schema="polygonSchema"
253
253
  />
254
254
  </template>
@@ -259,7 +259,7 @@ watch(
259
259
  v-model:attributeRange="polyhedron_attribute_range"
260
260
  v-model:attributeColorMap="polyhedron_attribute_color_map"
261
261
  :id="id"
262
- :componentId="componentId"
262
+ :componentIds="componentIds"
263
263
  :schema="polyhedronSchema"
264
264
  />
265
265
  </template>
@@ -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.14",
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,