@geode/opengeodeweb-front 10.34.0 → 10.35.0-rc.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.
@@ -160,16 +160,7 @@ var api = {
160
160
  "required": ["extension", "platform"],
161
161
  "additionalProperties": false
162
162
  } }
163
- },
164
- "serverless": { "run_cloud": {
165
- "$id": "api/serverless/run_cloud",
166
- "route": "/run_cloud",
167
- "methods": ["POST"],
168
- "type": "object",
169
- "properties": { "email": { "type": "string" } },
170
- "required": ["email"],
171
- "additionalProperties": false
172
- } }
163
+ }
173
164
  };
174
165
  //#endregion
175
166
  export { api };
@@ -1,77 +1,8 @@
1
1
  import path from "node:path";
2
2
  import fs from "node:fs";
3
3
  import { execFileSync } from "node:child_process";
4
- import { google } from "googleapis";
5
4
  //#region server/utils/cloud.ts
6
5
  const LOCATIONS_DIR = "/etc/nginx/locations";
7
- async function artifactImage(parent, authClient) {
8
- const projectName = process.env.PROJECT;
9
- const registry = google.artifactregistry({
10
- version: "v1",
11
- auth: authClient
12
- });
13
- const branch = process.env.NETLIFY_BRANCH;
14
- const [, projectId] = parent.split("/");
15
- const name = `${`${parent}/repositories/github/packages/`}${projectName}/tags/${branch}`;
16
- console.log({ name });
17
- const response = await registry.projects.locations.repositories.packages.tags.get({ name });
18
- console.log({ response });
19
- const digest = (response.data.version ?? "").split("/").pop();
20
- const image = `${`europe-west9-docker.pkg.dev/${projectId}/github`}/${projectName}@${digest}`;
21
- console.log("Found image for", projectName, image);
22
- return image;
23
- }
24
- function sanitizeLabelValue(label) {
25
- console.log("label", label);
26
- return label.toLowerCase().replaceAll(/[^a-z0-9_-]/gu, "_").slice(0, 63);
27
- }
28
- function requestConfig(parent, image, email, projectName) {
29
- const resources = {
30
- limits: {
31
- cpu: "2000m",
32
- memory: "3Gi"
33
- },
34
- cpuIdle: false,
35
- startupCpuBoost: true
36
- };
37
- const labels = {
38
- user: sanitizeLabelValue(email),
39
- project: sanitizeLabelValue(projectName)
40
- };
41
- return {
42
- parent,
43
- service: {
44
- ingress: "INGRESS_TRAFFIC_ALL",
45
- invokerIamDisabled: true,
46
- labels,
47
- scaling: {
48
- scalingMode: "MANUAL",
49
- manualInstanceCount: 1
50
- },
51
- template: {
52
- labels,
53
- timeout: { seconds: 3600 },
54
- containers: [{
55
- image,
56
- ports: [{ containerPort: 80 }],
57
- resources,
58
- env: [{
59
- name: "PARENT",
60
- value: parent
61
- }],
62
- startupProbe: {
63
- httpGet: {
64
- port: 80,
65
- path: "/viewer/healthcheck"
66
- },
67
- periodSeconds: 1,
68
- failureThreshold: 30
69
- }
70
- }]
71
- }
72
- }
73
- };
74
- }
75
6
  function addSupervisorProgram(name, command, executableArgs) {
76
7
  const conf = `
77
8
  [program:${name}]
@@ -153,4 +84,4 @@ function removeNginxLocation(name) {
153
84
  return true;
154
85
  }
155
86
  //#endregion
156
- export { addNginxLocation, addSupervisorProgram, artifactImage, removeNginxLocation, requestConfig };
87
+ export { addNginxLocation, addSupervisorProgram, removeNginxLocation };
@@ -0,0 +1,68 @@
1
+ import type { RequestHandlers } from "@ogw_shared/utils/types";
2
+ import { api_fetch } from "@ogw_internal/utils/api_fetch";
3
+
4
+ interface ApiSchema {
5
+ $id: string;
6
+ methods: string[];
7
+ [key: string]: unknown;
8
+ }
9
+
10
+ type ApiCallbacks = RequestHandlers & {
11
+ skip_feedback_error?: boolean;
12
+ };
13
+
14
+ const MILLISECONDS_IN_SECOND = 1000;
15
+
16
+ export const useAPIStore = defineStore("api", () => {
17
+ const request_counter = ref(0);
18
+ const base_url = ref(useRuntimeConfig().public.CLOUD_API_URL);
19
+
20
+ function start_request(): void {
21
+ request_counter.value += 1;
22
+ }
23
+
24
+ function stop_request(): void {
25
+ request_counter.value -= 1;
26
+ }
27
+
28
+ // `TResult` is asserted, not verified: the response is only checked against `schema` at runtime
29
+ async function request<TResult = unknown>(
30
+ {
31
+ schema,
32
+ params = {},
33
+ headers = {},
34
+ }: { schema: ApiSchema; params?: Record<string, unknown>; headers?: Record<string, string> },
35
+ callbacks: ApiCallbacks = {},
36
+ ): Promise<TResult> {
37
+ console.log("[API] Request:", schema.$id);
38
+ const start = Date.now();
39
+
40
+ const result = await api_fetch(
41
+ { $id: schema.$id, base_url: base_url.value, start_request, stop_request },
42
+ { schema, params, headers },
43
+ {
44
+ ...callbacks,
45
+ response_function: async (response: unknown) => {
46
+ console.log(
47
+ "[API] Request completed:",
48
+ schema.$id,
49
+ "in",
50
+ (Date.now() - start) / MILLISECONDS_IN_SECOND,
51
+ "s",
52
+ );
53
+ if (callbacks.response_function) {
54
+ await callbacks.response_function(response);
55
+ }
56
+ },
57
+ },
58
+ );
59
+ // oxlint-disable-next-line no-unsafe-type-assertion -- trusted API boundary; see comment above.
60
+ return result as TResult;
61
+ }
62
+ return {
63
+ base_url,
64
+ request,
65
+ start_request,
66
+ stop_request,
67
+ };
68
+ });
@@ -1,8 +1,19 @@
1
1
  import { Status } from "@ogw_front/utils/status";
2
- import { fetchSchema } from "@ogw_shared/utils/fetch_schema";
3
2
  import { setAppBaseUrl } from "@ogw_shared/scripts";
3
+ import { useAPIStore } from "@ogw_front/stores/api";
4
4
 
5
- import opengeodeweb_front_schemas from "@geode/opengeodeweb-front/opengeodeweb_front_schemas.json" with { type: "json" };
5
+ const run_cloud_schema = {
6
+ $id: "/cloud/run",
7
+ methods: ["POST"],
8
+ type: "object",
9
+ properties: {
10
+ email: { type: "string" },
11
+ project: { type: "string" },
12
+ branch: { type: "string" },
13
+ },
14
+ required: ["email", "project", "branch"],
15
+ additionalProperties: false,
16
+ };
6
17
 
7
18
  export const useCloudStore = defineStore("cloud", {
8
19
  state: () => ({
@@ -11,14 +22,15 @@ export const useCloudStore = defineStore("cloud", {
11
22
  actions: {
12
23
  async launch(email: string) {
13
24
  this.status = Status.CONNECTING;
14
- const schema = opengeodeweb_front_schemas.api.serverless.run_cloud;
15
- const params = { email };
25
+ const { PROJECT, BRANCH } = useRuntimeConfig().public;
26
+ const params = { email, project: PROJECT, branch: BRANCH };
16
27
  const { useAppStore } = await import("./app");
17
28
  const { useFeedbackStore } = await import("./feedback");
18
29
  const appStore = useAppStore();
19
30
  const feedbackStore = useFeedbackStore();
20
- const result = await fetchSchema(
21
- { schema, params },
31
+ const APIStore = useAPIStore();
32
+ const result = await APIStore.request(
33
+ { schema: run_cloud_schema, params },
22
34
  {
23
35
  request_error_function: () => {
24
36
  feedbackStore.$patch({ server_error: true });
package/nuxt.config.ts CHANGED
@@ -4,12 +4,57 @@ import path from "node:path";
4
4
 
5
5
  // Third party imports
6
6
  import { defineNuxtConfig } from "nuxt/config";
7
+ import type { Nuxt } from "nuxt/schema";
7
8
 
8
9
  // Local imports
9
- import package_json from "./package.json";
10
10
 
11
11
  const __dirname = import.meta.dirname;
12
12
 
13
+ const CLOUD_API_BASE_URL = "https://europe-west9-project-98b129be-91e9-491b-8ce.cloudfunctions.net";
14
+ const RELEASE_VERSION_REGEX = /^\d+\.\d+\.\d+$/u;
15
+
16
+ // Master builds ("latest" on cloud/docker, plain semver on desktop) target the
17
+ // Production api; next builds ("next", "x.y.z-rc.n") and local dev ("0.0.0")
18
+ // Target api-next.
19
+ function cloudApiUrl(version: string): string {
20
+ const is_master =
21
+ version === "latest" || (RELEASE_VERSION_REGEX.test(version) && version !== "0.0.0");
22
+ return `${CLOUD_API_BASE_URL}/${is_master ? "api" : "api-next"}`;
23
+ }
24
+
25
+ function isUnset(value: unknown): boolean {
26
+ return typeof value !== "string" || value === "";
27
+ }
28
+
29
+ function readAppPackage(root_dir: string): { name: string; version: string } {
30
+ const app_package: unknown = JSON.parse(
31
+ fs.readFileSync(path.join(root_dir, "package.json"), "utf8"),
32
+ );
33
+ if (typeof app_package !== "object" || app_package === null) {
34
+ return { name: "", version: "0.0.0" };
35
+ }
36
+ return {
37
+ name: "name" in app_package && typeof app_package.name === "string" ? app_package.name : "",
38
+ version:
39
+ "version" in app_package && typeof app_package.version === "string"
40
+ ? app_package.version
41
+ : "0.0.0",
42
+ };
43
+ }
44
+
45
+ // This file is also loaded as a layer, where ./package.json is the layer's own: the app's
46
+ // Package is only reachable from a module, once nuxt.options.rootDir points at the app.
47
+ function setAppPackageConfig(_options: unknown, nuxt: Nuxt): void {
48
+ const config = nuxt.options.runtimeConfig.public;
49
+ const { name, version } = readAppPackage(nuxt.options.rootDir);
50
+ if (isUnset(config.PROJECT)) {
51
+ config.PROJECT = name;
52
+ }
53
+ if (isUnset(config.CLOUD_API_URL)) {
54
+ config.CLOUD_API_URL = cloudApiUrl(version);
55
+ }
56
+ }
57
+
13
58
  // Oxlint's type-aware linter auto-discovers each file's nearest tsconfig.json
14
59
  // By walking up directories, and any "extends" on that discovered file makes
15
60
  // Its whole type-aware resolution collapse: every symbol coming through the
@@ -41,7 +86,9 @@ export default defineNuxtConfig({
41
86
  COMMAND_VIEWER: "opengeodeweb-viewer",
42
87
  NUXT_ROOT_PATH: __dirname,
43
88
  MODE: process.env.MODE ?? "CLOUD",
44
- PROJECT: package_json.name,
89
+ PROJECT: "",
90
+ BRANCH: process.env.NETLIFY_BRANCH ?? "next",
91
+ CLOUD_API_URL: process.env.CLOUD_API_URL ?? "",
45
92
  },
46
93
  },
47
94
 
@@ -49,6 +96,7 @@ export default defineNuxtConfig({
49
96
  "vuetify-nuxt-module",
50
97
  ["@pinia/nuxt", { autoImports: ["defineStore", "storeToRefs"] }],
51
98
  "@vueuse/nuxt",
99
+ setAppPackageConfig,
52
100
  ],
53
101
  imports: {
54
102
  scan: false,
@@ -210,21 +210,6 @@
210
210
  "additionalProperties": false
211
211
  }
212
212
  }
213
- },
214
- "serverless": {
215
- "run_cloud": {
216
- "$id": "api/serverless/run_cloud",
217
- "route": "/run_cloud",
218
- "methods": ["POST"],
219
- "type": "object",
220
- "properties": {
221
- "email": {
222
- "type": "string"
223
- }
224
- },
225
- "required": ["email"],
226
- "additionalProperties": false
227
- }
228
213
  }
229
214
  }
230
215
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geode/opengeodeweb-front",
3
- "version": "10.34.0",
3
+ "version": "10.35.0-rc.1",
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": {
@@ -62,9 +62,8 @@
62
62
  "prepublishOnly": "npm run build"
63
63
  },
64
64
  "dependencies": {
65
- "@geode/opengeodeweb-back": "latest",
66
- "@geode/opengeodeweb-viewer": "latest",
67
- "@google-cloud/run": "4.1.0",
65
+ "@geode/opengeodeweb-back": "next",
66
+ "@geode/opengeodeweb-viewer": "next",
68
67
  "@kitware/vtk.js": "33.3.0",
69
68
  "@mdi/font": "7.4.47",
70
69
  "@pinia/nuxt": "1.0.1",
@@ -80,8 +79,6 @@
80
79
  "conf": "15.1.0",
81
80
  "dexie": "4.2.1",
82
81
  "get-port-please": "3.2.0",
83
- "google-auth-library": "11.0.2",
84
- "googleapis": "181.0.0",
85
82
  "js-file-download": "0.4.12",
86
83
  "jszip": "3.10.1",
87
84
  "lodash": "4.18.1",
@@ -4,111 +4,11 @@ import fs from "node:fs";
4
4
  import path from "node:path";
5
5
 
6
6
  // Third party imports
7
- import type { AuthClient, OAuth2Client } from "google-auth-library";
8
- import { google } from "googleapis";
9
- import type { protos } from "@google-cloud/run";
10
7
 
11
8
  // Local imports
12
9
 
13
- // oxlint-disable-next-line eslint/id-length
14
- type CreateServiceRequest = protos.google.cloud.run.v2.ICreateServiceRequest;
15
-
16
10
  const LOCATIONS_DIR = "/etc/nginx/locations";
17
11
 
18
- async function artifactImage(parent: string, authClient: AuthClient): Promise<string> {
19
- const projectName = process.env.PROJECT;
20
- const registry = google.artifactregistry({
21
- version: "v1",
22
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion
23
- auth: authClient as OAuth2Client,
24
- });
25
- const branch = process.env.NETLIFY_BRANCH;
26
- const [, projectId] = parent.split("/");
27
- const repository = `${parent}/repositories/github/packages/`;
28
- const name = `${repository}${projectName}/tags/${branch}`;
29
- console.log({ name });
30
- const response = await registry.projects.locations.repositories.packages.tags.get({
31
- name,
32
- });
33
- console.log({ response });
34
- const version = response.data.version ?? "";
35
- const digest = version.split("/").pop();
36
- const artifactRegistry = `europe-west9-docker.pkg.dev/${projectId}/github`;
37
- const image = `${artifactRegistry}/${projectName}@${digest}`;
38
- console.log("Found image for", projectName, image);
39
- return image;
40
- }
41
-
42
- function sanitizeLabelValue(label: string): string {
43
- console.log("label", label);
44
- const maxLabelLength = 63;
45
- return label
46
- .toLowerCase()
47
- .replaceAll(/[^a-z0-9_-]/gu, "_")
48
- .slice(0, maxLabelLength);
49
- }
50
-
51
- function requestConfig(
52
- parent: string,
53
- image: string,
54
- email: string,
55
- projectName: string,
56
- ): CreateServiceRequest {
57
- const resources = {
58
- limits: {
59
- cpu: "2000m",
60
- memory: "3Gi",
61
- },
62
- cpuIdle: false,
63
- startupCpuBoost: true,
64
- };
65
- const labels = {
66
- user: sanitizeLabelValue(email),
67
- project: sanitizeLabelValue(projectName),
68
- };
69
- return {
70
- parent,
71
- service: {
72
- ingress: "INGRESS_TRAFFIC_ALL",
73
- invokerIamDisabled: true,
74
- labels,
75
- scaling: {
76
- scalingMode: "MANUAL",
77
- manualInstanceCount: 1,
78
- },
79
- template: {
80
- labels,
81
- timeout: { seconds: 3600 },
82
- containers: [
83
- {
84
- image,
85
- ports: [
86
- {
87
- containerPort: 80,
88
- },
89
- ],
90
- resources,
91
- env: [
92
- {
93
- name: "PARENT",
94
- value: parent,
95
- },
96
- ],
97
- startupProbe: {
98
- httpGet: {
99
- port: 80,
100
- path: "/viewer/healthcheck",
101
- },
102
- periodSeconds: 1,
103
- failureThreshold: 30,
104
- },
105
- },
106
- ],
107
- },
108
- },
109
- };
110
- }
111
-
112
12
  function addSupervisorProgram(
113
13
  name: string,
114
14
  command: string,
@@ -211,10 +111,4 @@ function removeNginxLocation(name: string): boolean {
211
111
  return true;
212
112
  }
213
113
 
214
- export {
215
- addNginxLocation,
216
- addSupervisorProgram,
217
- artifactImage,
218
- removeNginxLocation,
219
- requestConfig,
220
- };
114
+ export { addNginxLocation, addSupervisorProgram, removeNginxLocation };
@@ -1,92 +0,0 @@
1
- // Node imports
2
-
3
- // Third party imports
4
- import { GoogleAuth, type JWTInput } from "google-auth-library";
5
- import { type H3Event, createError, defineEventHandler, readBody } from "h3";
6
- import { ServicesClient } from "@google-cloud/run";
7
-
8
- // Local imports
9
- import { artifactImage, requestConfig } from "@geode/opengeodeweb-front/server/utils/cloud.ts";
10
-
11
- interface RunCloudBody {
12
- email: string;
13
- }
14
-
15
- function isServiceAccountCredentials(value: unknown): value is JWTInput {
16
- return (
17
- typeof value === "object" && value !== null && "client_email" in value && "private_key" in value
18
- );
19
- }
20
-
21
- export default defineEventHandler(async (event: H3Event) => {
22
- try {
23
- const { email } = await readBody<RunCloudBody>(event);
24
- console.log("[RUN CLOUD] Received request to create backend for email:", email);
25
-
26
- const { GOOGLE_CLOUD_KEY, GOOGLE_CLOUD_PROJECT, PROJECT } = process.env;
27
-
28
- if (
29
- GOOGLE_CLOUD_KEY === undefined ||
30
- GOOGLE_CLOUD_KEY === "" ||
31
- GOOGLE_CLOUD_PROJECT === undefined ||
32
- GOOGLE_CLOUD_PROJECT === "" ||
33
- PROJECT === undefined ||
34
- PROJECT === ""
35
- ) {
36
- throw createError({
37
- statusCode: 500,
38
- statusMessage:
39
- "Missing required environment variables: GOOGLE_CLOUD_KEY, GOOGLE_CLOUD_PROJECT, or PROJECT",
40
- });
41
- }
42
-
43
- const projectId: string = GOOGLE_CLOUD_PROJECT;
44
- const projectName: string = PROJECT;
45
-
46
- const parsedKey: unknown = JSON.parse(GOOGLE_CLOUD_KEY);
47
- if (!isServiceAccountCredentials(parsedKey)) {
48
- throw createError({
49
- statusCode: 500,
50
- statusMessage: "GOOGLE_CLOUD_KEY is not a valid service account credentials JSON",
51
- });
52
- }
53
- const credentials: JWTInput = parsedKey;
54
-
55
- const location = "europe-west9";
56
- const parent = `projects/${projectId}/locations/${location}`;
57
- console.log({ parent });
58
-
59
- const auth = new GoogleAuth({
60
- // oxlint-disable-next-line typescript/no-deprecated
61
- credentials,
62
- scopes: ["https://www.googleapis.com/auth/cloud-platform"],
63
- });
64
- const authClient = await auth.getClient();
65
- const image = await artifactImage(parent, authClient);
66
- const request = requestConfig(parent, image, email, projectName);
67
- console.log({ request });
68
-
69
- const runClient = new ServicesClient({ authClient });
70
- const [operation] = await runClient.createService(request);
71
- const [response] = await operation.promise();
72
-
73
- if (response.uri === null || response.uri === undefined || response.uri === "") {
74
- throw createError({
75
- statusCode: 500,
76
- statusMessage: "Cloud Run service created but no URI was returned",
77
- });
78
- }
79
-
80
- console.log("Service URL created:", response.uri);
81
- return {
82
- statusCode: 200,
83
- url: response.uri.replace(/^https?:\/\//iu, ""),
84
- };
85
- } catch (error) {
86
- console.log(error);
87
- throw createError({
88
- statusCode: 500,
89
- statusMessage: error instanceof Error ? error.message : String(error),
90
- });
91
- }
92
- });
@@ -1,10 +0,0 @@
1
- {
2
- "route": "/run_cloud",
3
- "methods": ["POST"],
4
- "type": "object",
5
- "properties": {
6
- "email": { "type": "string" }
7
- },
8
- "required": ["email"],
9
- "additionalProperties": false
10
- }