From d6acf825c8529c1e5bccb6c1d4a8952685521485 Mon Sep 17 00:00:00 2001
From: robin <binkrassdufass@gmail.com>
Date: Wed, 6 Dec 2023 17:57:25 +0000
Subject: [PATCH] test

---
 .gitea/workflows/deploy.yaml | 100 +++++++++----------
 api/collections/ssr.yml      |   2 +
 api/config.yml               |   1 +
 api/hooks/config-client.js   |   3 +-
 api/hooks/lib/ssr.js         | 180 +++++++++++++++++++++++++++++++++++
 api/hooks/ssr/get_read.js    |   2 +-
 esbuild.config.js            |  34 ++++++-
 frontend/.htaccess           |   7 +-
 frontend/src/api.ts          | 119 +----------------------
 frontend/src/ssr.ts          |   2 +-
 types/global.d.ts            |  21 ++++
 11 files changed, 299 insertions(+), 172 deletions(-)
 create mode 100644 api/hooks/lib/ssr.js

diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml
index db1a043..a6cecd8 100644
--- a/.gitea/workflows/deploy.yaml
+++ b/.gitea/workflows/deploy.yaml
@@ -89,11 +89,11 @@ jobs:
               run: |
                   yarn build
 
-            #- name: build ssr
-            #  env:
-            #      FORCE_COLOR: "true"
-            #  run: |
-            #      yarn build:server
+            - name: build ssr
+              env:
+                  FORCE_COLOR: "true"
+              run: |
+                  yarn build:server
 
             - name: build legacy
               env:
@@ -101,58 +101,58 @@ jobs:
               run: |
                   yarn build:legacy
 
-            - name: Wait for Live Server
-              run: |
-                  attempts=0
-                  max_attempts=2
-                  while ! curl --output /dev/null --silent --head --fail http://live-server:8081; do
-                  if [ $attempts -eq $max_attempts ]; then
-                            echo "Live server not ready after $max_attempts attempts"
-                            echo "${{ toJson(job) }}"
-                            curl -v http://live-server:8081
-                      exit 1
-                  fi
-                  attempts=$((attempts+1))
-                  echo "Waiting for live-server to be ready... attempt $attempts"
-                  sleep 5
-                  done
+            #- name: Wait for Live Server
+            #  run: |
+            #      attempts=0
+            #      max_attempts=2
+            #      while ! curl --output /dev/null --silent --head --fail http://live-server:8081; do
+            #     if [ $attempts -eq $max_attempts ]; then
+            #                echo "Live server not ready after $max_attempts attempts"
+            #                echo "${{ toJson(job) }}"
+            #                curl -v http://live-server:8081
+            #          exit 1
+            #      fi
+            #      attempts=$((attempts+1))
+            #      echo "Waiting for live-server to be ready... attempt $attempts"
+            #      sleep 5
+            #      done
 
-            - name: Test HTTP Request
-              run: |
-                  echo "Live server not ready after $max_attempts attempts"
-                  echo "${{ toJson(job) }}"
-                  echo "${{ job.services.live-server.id }}"
-                  echo "${{ job.services.tibi-server.id }}"
-                  echo "${{ job.services.mongo.id }}"
+            #- name: Test HTTP Request
+            #  run: |
+            #      echo "Live server not ready after $max_attempts attempts"
+            #      echo "${{ toJson(job) }}"
+            #      echo "${{ job.services.live-server.id }}"
+            #      echo "${{ job.services.tibi-server.id }}"
+            ##      echo "${{ job.services.mongo.id }}"
 
-                  docker logs "${{ job.services.tibi-server.id }}"
-                  docker logs "${{ job.services.live-server.id }}"
-                  curl -v http://live-server:8081
+            #      docker logs "${{ job.services.tibi-server.id }}"
+            #      docker logs "${{ job.services.live-server.id }}"
+            #      curl -v http://live-server:8081
 
-            - name: Install Chrome
-              run: |
-                  wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
-                  sudo sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list'
-
-                  sudo apt-get update
-                  sudo apt-get install -y google-chrome-stable
+            #- name: Install Chrome
+            #  run: |
+            #      wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
+            #     sudo sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list'
+            #
+            #      sudo apt-get update
+            #      sudo apt-get install -y google-chrome-stable
 
             # Lighthouse Analysis Step
-            - name: Lighthouse Analysis
-              run: |
-                  yarn add lighthouse
-                  npx lighthouse http://127.0.0.1:8081 --output json --output-path /tmp/lighthouse-report.json --chrome-flags="--headless --no-sandbox --disable-dev-shm-usage"
+            #- name: Lighthouse Analysis
+            #  run: |
+            #     yarn add lighthouse
+            #      npx lighthouse http://127.0.0.1:8081 --output json --output-path /tmp/lighthouse-report.json --chrome-flags="--headless --no-sandbox --disable-dev-shm-usage"
 
             # Notify-Lighthouse Step
-            - name: Notify Lighthouse
-              run: |
-                  docker run --rm \
-                  -e PLUGIN_FROM=noreply@gitbase.de \
-                  -e PLUGIN_HOST=smtp.basehosts.de \
-                  -e PLUGIN_RECIPIENT=recipient@example.com \
-                  -e PLUGIN_SUBJECT="Lighthouse Report" \
-                  -v ${{ github.workspace }}/tmp:/lighthouse-reports \
-                  drillster/drone-email /tmp/lighthouse-report.json
+            #- name: Notify Lighthouse
+            #  run: |
+            #      docker run --rm \
+            #      -e PLUGIN_FROM=noreply@gitbase.de \
+            #      -e PLUGIN_HOST=smtp.basehosts.de \
+            #      -e PLUGIN_RECIPIENT=recipient@example.com \
+            #      -e PLUGIN_SUBJECT="Lighthouse Report" \
+            #      -v ${{ github.workspace }}/tmp:/lighthouse-reports \
+            #      drillster/drone-email /tmp/lighthouse-report.json
 
             - name: deploy
               if: github.ref == 'refs/heads/master'
diff --git a/api/collections/ssr.yml b/api/collections/ssr.yml
index 5dde647..c458313 100644
--- a/api/collections/ssr.yml
+++ b/api/collections/ssr.yml
@@ -7,6 +7,7 @@ meta:
     label: { de: "SSR Dummy", en: "ssr dummy" }
     muiIcon: server
     rowIdentTpl: { twig: "{{ id }}" }
+    
     views:
         - type: simpleList
           mediaQuery: "(max-width: 600px)"
@@ -55,6 +56,7 @@ fields:
     - name: path
       type: string
       index: [single, unique]
+
     - name: content
       type: string
       meta:
diff --git a/api/config.yml b/api/config.yml
index fe7fffe..ca04f9c 100644
--- a/api/config.yml
+++ b/api/config.yml
@@ -38,6 +38,7 @@ collections:
     - !include collections/banner.yml
     - !include collections/forms.yml
     - !include collections/backups.yml
+    - !include collections/ssr.yml
 
 jobs:
     - cron: "0 * * * *"
diff --git a/api/hooks/config-client.js b/api/hooks/config-client.js
index 0736b02..51b3fd7 100644
--- a/api/hooks/config-client.js
+++ b/api/hooks/config-client.js
@@ -1,5 +1,5 @@
 const release = "tibi-docs.dirty"
-
+const apiClientBaseURL = "/api/"
 // @ts-ignore
 if (release && typeof context !== "undefined") {
     context.response.header("X-Release", release)
@@ -7,4 +7,5 @@ if (release && typeof context !== "undefined") {
 
 module.exports = {
     release,
+    apiClientBaseURL,
 }
diff --git a/api/hooks/lib/ssr.js b/api/hooks/lib/ssr.js
new file mode 100644
index 0000000..e97f4fb
--- /dev/null
+++ b/api/hooks/lib/ssr.js
@@ -0,0 +1,180 @@
+const { apiClientBaseURL } = require("../config-client")
+
+/**
+ * convert object to string
+ * @param {any} obj object
+ */
+function obj2str(obj) {
+    if (Array.isArray(obj)) {
+        return JSON.stringify(
+            obj.map(function (idx) {
+                return obj2str(idx)
+            })
+        )
+    } else if (typeof obj === "object" && obj !== null) {
+        var elements = Object.keys(obj)
+            .sort()
+            .map(function (key) {
+                var val = obj2str(obj[key])
+                if (val) {
+                    return key + ":" + val
+                }
+            })
+
+        var elementsCleaned = []
+        for (var i = 0; i < elements.length; i++) {
+            if (elements[i]) elementsCleaned.push(elements[i])
+        }
+
+        return "{" + elementsCleaned.join("|") + "}"
+    }
+
+    if (obj) return obj
+}
+
+// fetch polyfill
+// [MIT License](LICENSE.md) © [Jason Miller](https://jasonformat.com/)
+const _f = function (
+    /** @type {string | URL} */ url,
+    /** @type {{ method?: any; credentials?: any; headers?: any; body?: any; }} */ options
+) {
+    if (typeof XMLHttpRequest === "undefined") {
+        return Promise.resolve(null)
+    }
+
+    options = options || {}
+    return new Promise((resolve, reject) => {
+        const request = new XMLHttpRequest()
+        const keys = []
+        const all = []
+        const headers = {}
+
+        const response = () => ({
+            ok: ((request.status / 100) | 0) == 2, // 200-299
+            statusText: request.statusText,
+            status: request.status,
+            url: request.responseURL,
+            text: () => Promise.resolve(request.responseText),
+            json: () => Promise.resolve(request.responseText).then(JSON.parse),
+            blob: () => Promise.resolve(new Blob([request.response])),
+            clone: response,
+            headers: {
+                // @ts-ignore
+                keys: () => keys,
+                // @ts-ignore
+                entries: () => all,
+                get: (n) => headers[n.toLowerCase()],
+                has: (n) => n.toLowerCase() in headers,
+            },
+        })
+
+        request.open(options.method || "get", url, true)
+
+        request.onload = () => {
+            request
+                .getAllResponseHeaders()
+                // @ts-ignore
+                .replace(/^(.*?):[^\S\n]*([\s\S]*?)$/gm, (m, key, value) => {
+                    keys.push((key = key.toLowerCase()))
+                    all.push([key, value])
+                    headers[key] = headers[key] ? `${headers[key]},${value}` : value
+                })
+            resolve(response())
+        }
+
+        request.onerror = reject
+
+        request.withCredentials = options.credentials == "include"
+
+        for (const i in options.headers) {
+            request.setRequestHeader(i, options.headers[i])
+        }
+
+        request.send(options.body || null)
+    })
+}
+
+const _fetch = typeof fetch === "undefined" ? (typeof window === "undefined" ? _f : window.fetch || _f) : fetch
+
+/**
+ * api request via client or server
+ * server function ssrRequest is called via context.ssrRequest, binded in ssr hook
+ *
+ * @param {string} endpoint
+ * @param {ApiOptions} options
+ * @param {any} body
+ * @returns {Promise<ApiResult<any>>}
+ */
+function apiRequest(endpoint, options, body) {
+    // TODO cache only for GET
+
+    // first check cache if on client
+    const cacheKey = obj2str({ endpoint: endpoint, options: options })
+
+    // @ts-ignore
+    if (typeof window !== "undefined" && window.__SSR_CACHE__ && options?.method === "GET") {
+        // @ts-ignore
+        const cache = window.__SSR_CACHE__[cacheKey]
+        console.log("SSR:", cacheKey, cache)
+        if (cache) {
+            return Promise.resolve(cache)
+        }
+    }
+
+    let method = options?.method || "GET"
+
+    let query = "&count=1"
+    if (options?.filter) query += "&filter=" + encodeURIComponent(JSON.stringify(options.filter))
+    if (options?.sort) query += "&sort=" + options.sort + "&sort=_id"
+    if (options?.limit) query += "&limit=" + options.limit
+    if (options?.offset) query += "&offset=" + options.offset
+    if (options?.projection) query += "&projection=" + options.projection
+    if (options?.lookup) query += "&lookup=" + options.lookup
+
+    if (options?.params) {
+        Object.keys(options.params).forEach((p) => {
+            query += "&" + p + "=" + encodeURIComponent(options.params[p])
+        })
+    }
+
+    let headers = {
+        "Content-Type": "application/json",
+    }
+
+    if (options?.headers) headers = { ...headers, ...options.headers }
+
+    if (typeof window === "undefined" && method === "GET") {
+        // server
+
+        // reference via context from get hook to tree shake in client
+        // @ts-ignore
+        const d = context.ssrRequest(cacheKey, endpoint, query, Object.assign({}, options, { method, headers }))
+        return d
+    } else {
+        // client
+        let url = endpoint + (query ? "?" + query : "")
+        const requestOptions = {
+            method,
+            mode: "cors",
+            headers,
+        }
+
+        if (method === "POST" || method === "PUT") {
+            requestOptions.body = JSON.stringify(body)
+        }
+
+        return _fetch(apiClientBaseURL + url, requestOptions).then((response) => {
+            return response?.json().then((json) => {
+                if (response?.status < 200 || response?.status >= 400) {
+                    return Promise.reject({ response, data: json })
+                }
+                return Promise.resolve({ data: json || null, count: response.headers?.get("x-results-count") || 0 })
+            })
+        })
+    }
+}
+
+module.exports = {
+    obj2str,
+    apiRequest,
+}
diff --git a/api/hooks/ssr/get_read.js b/api/hooks/ssr/get_read.js
index 90bff2e..0899f03 100644
--- a/api/hooks/ssr/get_read.js
+++ b/api/hooks/ssr/get_read.js
@@ -4,7 +4,7 @@ const { obj2str, log } = require("../lib/utils")
 ;(function () {
     /** @type {HookResponse} */
     var response = null
-
+    console.log("SSR GET READ")
     var request = context.request()
     var url = request.query("url")
     var noCache = request.query("noCache")
diff --git a/esbuild.config.js b/esbuild.config.js
index c8be6d2..7a474e7 100644
--- a/esbuild.config.js
+++ b/esbuild.config.js
@@ -1,3 +1,5 @@
+const fs = require("fs")
+
 const resolvePlugin = {
     name: "resolvePlugin",
     setup(build) {
@@ -68,7 +70,9 @@ const bsMiddleware = []
 
 if (process.argv[2] == "start") {
     const { createProxyMiddleware } = require("http-proxy-middleware")
-    const apiBase = process.env.API_BASE || "http://localhost:8080/api/v1/_/" + process.env.NAMESPACE
+    const dotEnv = fs.readFileSync(__dirname + "/.env", "utf8")
+    const TIBI_NAMESPACE = dotEnv.match(/TIBI_NAMESPACE=(.*)/)[1]
+    const apiBase = process.env.API_BASE || "http://localhost:8080/api/v1/_/" + TIBI_NAMESPACE
     bsMiddleware.push(
         createProxyMiddleware("/api", {
             target: apiBase,
@@ -77,12 +81,39 @@ if (process.argv[2] == "start") {
             logLevel: "debug",
         })
     )
+
+    // if SSR env variable is set
+    console.log(process.env)
+    if (process.env.SSR) {
+        // read api/config.yml.env and read SSR_TOKEN variable from it
+        const configEnv = fs.readFileSync(__dirname + "/api/config.yml.env", "utf8")
+        const SSR_TOKEN = configEnv.match(/SSR_TOKEN=(.*)/)[1]
+
+        // redirect all other requests to /api/ssr?token=owshwerNwoa&url=...
+        bsMiddleware.push(
+            createProxyMiddleware(
+                function (path, req) {
+                    return !path.match(/\./)
+                },
+                {
+                    target: apiBase,
+                    changeOrigin: true,
+                    logLevel: "debug",
+                    pathRewrite: function (path, req) {
+                        console.log(path)
+                        return "/ssr?token=" + SSR_TOKEN + "&url=" + encodeURIComponent(path)
+                    },
+                }
+            )
+        )
+    }
 }
 
 module.exports = {
     sveltePlugin: sveltePlugin,
     resolvePlugin: resolvePlugin,
     options: options,
+    distDir,
     watch: {
         path: [__dirname + "/" + frontendDir + "/src/**/*"],
     },
@@ -103,7 +134,6 @@ module.exports = {
                 }),
             ],
         },
-        ghostMode: false,
         open: false,
         // logLevel: "debug",
     },
diff --git a/frontend/.htaccess b/frontend/.htaccess
index 5a6b99e..b1409a7 100644
--- a/frontend/.htaccess
+++ b/frontend/.htaccess
@@ -1,7 +1,7 @@
 AddType application/javascript .mjs
 
-DirectoryIndex spa.html
-#DirectoryIndex noindex
+#DirectoryIndex spa.html
+DirectoryIndex noindex
 
 <ifModule mod_rewrite.c>
     RewriteEngine On
@@ -11,5 +11,6 @@ DirectoryIndex spa.html
 
     RewriteCond %{REQUEST_FILENAME} !-f
     RewriteCond %{REQUEST_FILENAME} !-d
-    RewriteRule (.*) /spa.html [QSA,L]
+    RewriteRule ^/?(.*)$ http://tibi-server:8080/api/v1/_/allkids_erfurt/ssr?token=owshwerNwoa&url=/$1 [P,QSA,L]
+    #RewriteRule (.*) /spa.html [QSA,L]
 </ifModule>
diff --git a/frontend/src/api.ts b/frontend/src/api.ts
index fc5308d..934069f 100644
--- a/frontend/src/api.ts
+++ b/frontend/src/api.ts
@@ -1,121 +1,12 @@
-import { apiBaseURL } from "./config"
-
-const _f = function (url, options): Promise<Response> {
-    if (typeof XMLHttpRequest === "undefined") {
-        return Promise.resolve(null)
-    }
-
-    options = options || {}
-    return new Promise((resolve, reject) => {
-        const request = new XMLHttpRequest()
-        const keys = []
-        const all = []
-        const headers = {}
-
-        const response = (): Response => ({
-            ok: ((request.status / 100) | 0) == 2, // 200-299
-            statusText: request.statusText,
-            status: request.status,
-            url: request.responseURL,
-            text: () => Promise.resolve(request.responseText),
-            json: () => Promise.resolve(request.responseText).then(JSON.parse),
-            blob: () => Promise.resolve(new Blob([request.response])),
-            clone: response,
-            headers: {
-                // @ts-ignore
-                keys: () => keys,
-                // @ts-ignore
-                entries: () => all,
-                get: (n) => headers[n.toLowerCase()],
-                has: (n) => n.toLowerCase() in headers,
-            },
-        })
-
-        request.open(options.method || "get", url, true)
-
-        request.onload = () => {
-            request
-                .getAllResponseHeaders()
-                // @ts-ignore
-                .replace(/^(.*?):[^\S\n]*([\s\S]*?)$/gm, (m, key, value) => {
-                    keys.push((key = key.toLowerCase()))
-                    all.push([key, value])
-                    headers[key] = headers[key] ? `${headers[key]},${value}` : value
-                })
-            resolve(response())
-        }
-
-        request.onerror = reject
-        request.withCredentials = options.credentials == "include"
-        for (const i in options.headers) {
-            request.setRequestHeader(i, options.headers[i])
-        }
-        request.send(options.body || null)
-    })
-}
-
-const _fetch = typeof fetch === "undefined" ? (typeof window === "undefined" ? _f : window.fetch || _f) : fetch
+import { apiRequest } from "../../api/hooks/lib/ssr"
 
 export const api = async <T>(
     endpoint: string,
-    options?: {
-        method?: string
-        filter?: any
-        sort?: string
-        limit?: number
-        offset?: number
-        projection?: string
-        headers?: {
-            [key: string]: string
-        }
-        params?: {
-            [key: string]: string
-        }
-    },
+    options?: ApiOptions,
     body?: any
 ): Promise<{ data: T; count: number } | any> => {
-    if (typeof window === "undefined") {
-        // ssr
-        // @ts-ignore
-        return context.ssrFetch(endpoint, options)
-    }
-
-    let method = options?.method || "GET"
-
-    let query = "&count=1"
-    if (options?.filter) query += "&filter=" + encodeURIComponent(JSON.stringify(options.filter))
-    if (options?.sort) query += "&sort=" + options.sort + "&sort=_id"
-    if (options?.limit) query += "&limit=" + options.limit
-    if (options?.offset) query += "&offset=" + options.offset
-    if (options?.projection) query += "&projection=" + options.projection
-
-    if (options?.params) {
-        Object.keys(options.params).forEach((p) => {
-            query += "&" + p + "=" + encodeURIComponent(options.params[p])
-        })
-    }
-
-    let headers: any = {
-        "Content-Type": "application/json",
-    }
-
-    if (options?.headers) headers = { ...headers, ...options.headers }
-
-    let url = apiBaseURL + endpoint + (query ? "?" + query : "")
-
-    const requestOptions: any = {
-        method,
-        mode: "cors",
-        headers,
-    }
-
-    if (method === "POST" || method === "PUT") {
-        requestOptions.body = JSON.stringify(body)
-    }
-
-    let response = await _fetch(url, requestOptions)
-    if (response.status == 409 || response.status == 401) return response
-    let data = (await response?.json()) || null
+    let data = await apiRequest(endpoint, options, body)
     // @ts-ignore
-    return { data }
+    console.log(data, "data")
+    return data
 }
diff --git a/frontend/src/ssr.ts b/frontend/src/ssr.ts
index a4eea78..1add5ee 100644
--- a/frontend/src/ssr.ts
+++ b/frontend/src/ssr.ts
@@ -1,3 +1,3 @@
-import App from "./components/App.svelte"
+import App from "./App.svelte"
 
 export default App
diff --git a/types/global.d.ts b/types/global.d.ts
index 2ff9c18..7c4c1a1 100644
--- a/types/global.d.ts
+++ b/types/global.d.ts
@@ -2,6 +2,27 @@ interface Sites {
     [key: string]: Site
 }
 
+interface ApiResult<T> {
+    data: T
+    count: number
+}
+
+interface ApiOptions {
+    method?: string
+    filter?: any
+    sort?: string
+    lookup?: string
+    limit?: number
+    offset?: number
+    projection?: string
+    headers?: {
+        [key: string]: string
+    }
+    params?: {
+        [key: string]: string
+    }
+}
+
 interface Site {
     path: string
     showTeaser: boolean