backend and api endpoints

This commit is contained in:
Robin Grenzdörfer 2023-07-14 11:58:27 +00:00
parent 90b4c95cd8
commit 897b9ae2cf
27 changed files with 1140 additions and 20 deletions

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,92 @@
name: page
uploadPath: ../media/page
meta:
label: Inhalt
muiIcon: web
views:
- type: table
columns:
- source: path
tablist:
activeTab: site
tabs:
- name: general
label: Allgemein
subFields:
- source: path
- name: teaser
label: Teaser
subFields:
- source: teaser
- name: site
label: Seite
subFields:
- source: rows
imageFilter:
xs:
- fit: true
height: 90
width: 90
resampling: lanczos
quality: 60
s:
- fit: true
height: 300
width: 300
resampling: lanczos
quality: 60
m:
- fit: true
height: 600
width: 600
resampling: lanczos
quality: 60
l:
- fit: true
height: 1240
width: 1240
resampling: lanczos
quality: 60
xl:
- fit: true
height: 2000
width: 2000
resampling: lanczos
quality: 60
permissions:
public:
methods:
get: true
post: false
put: false
delete: false
user:
methods:
get: true
post: true
put: true
delete: true
projections:
navigation:
select:
path: 1
fields:
- type: string
name: path
meta:
label: Pfad
helperText: "Ein Pfad sollte mit einem / starten und ohne eins enden."
- !include fields/teaserHomepage.yml
- name: rows
type: object[]
meta:
label: Zeilen
subFields:
- !include fields/row.yml

View File

@ -0,0 +1,11 @@
- name: icon
type: file
meta:
label: Icon
helperText: "Das Icon wird in der Box angezeigt."
- name: text
type: string
meta:
label: Text
helperText: "Der Text wird in der Box angezeigt."

View File

@ -0,0 +1,37 @@
- name: image
type: file
meta:
label: Kartenausschnitt
helperText: "Der Kartenausschnitt wird als Hintergrundbild angezeigt."
- name: title
type: string
meta:
label: Dieser Titel wird in der Karte angezeigt (Name des Landes).
- name: properties
type: number[]
meta:
label: Eigenschaften
widget: select
choices:
- name: Aktien
id: 0
- name: Private Equity
id: 1
- name: Infrastruktur
id: 2
- name: Waldwirtschaft
id: 3
- name: Immobilienentwicklung
id: 4
- name: Renten
id: 5
- name: Venture Capital
id: 6
- name: Private Debt
id: 7
- name: Landwirtschaft
id: 8
- name: Immobilienbestand
id: 9

View File

@ -0,0 +1,349 @@
- name: contentType
type: string
meta:
label: ""
widget: select
choices:
- name: Bild
id: image
- name: Icons im Rechteck
id: iconCycleSquare
- name: Icons im Kreis
id: iconCycleCircle
- name: Text
id: text
- name: Informationsbrett
id: infoBoard
- name: Weltkarte
id: worldCard
- name: Verschatelte Karte
id: nestedCard
- name: Top-Down
id: topDown
- name: Personenvorschau
id: personPreview
- name: Boxliste
id: boxlist
- name: Ausfahrbare Box
id: extendableBox
- name: Text mit Link
id: textLink
- name: Icon block
id: iconBlocks
- name: Seitenlinks
id: pageLinkBlocks
- name: networkEvents
id: networkEvents
- name: publication
id: publication
- name: networkEvents
type: object[]
meta:
label: Netzwerkveranstaltungen
dependsOn:
eval: $parent.contentType == 'networkEvents'
subFields:
- name: beginDate
type: date
meta:
label: Beginn
- name: endDate
type: date
meta:
label: Ende
- name: title
type: string
meta:
label: Titel
- name: file
type: file
meta:
label: downloadDatei
- name: publication
type: object
meta:
label: Publikationen
dependsOn:
eval: $parent.contentType == 'publication'
subFields:
- name: content
type: string
meta:
label: Inhalt
- name: file
type: file
meta:
label: downloadDatei
- name: iconBlocks
type: object[]
meta:
label: Icon block
dependsOn:
eval: $parent.contentType == 'iconBlocks'
subFields:
- name: icon
type: file
meta:
label: Icon
- name: bigText
type: string
meta:
label: oberer text
- name: smallText
type: string
meta:
label: unterer Text
- name: pageLinkBlocks
type: object[]
meta:
label: Seitenlinks
dependsOn:
eval: $parent.contentType == 'pageLinkBlocks'
subFields:
- name: page
type: string
meta:
label: Seite
widget: select
choices:
endpoint: page
params:
sort: path
projection: navigation
mapping:
id: id
name: path
- name: name
type: string
meta:
label: Name
- name: rowNr
type: number
meta:
label: Zeilen Nr (0 Basiert)
- name: image
type: file
meta:
label: Bild
dependsOn:
eval: $parent.contentType == 'image'
- name: iconCycleSquare
type: object
meta:
label: Icons im Rechteck
dependsOn:
eval: $parent.contentType == 'iconCycleSquare'
subFields: !include iconCycleSquare.yml
- name: iconCycleCircle
type: object
meta:
label: Icons im Kreis
dependsOn:
eval: $parent.contentType == 'iconCycleCircle'
subFields: !include iconCycleCircle.yml
- name: text
type: string
meta:
widget: richtext
label: Text
dependsOn:
eval: $parent.contentType == 'text'
- name: infoBoard
type: object
meta:
label: Informationsbrett
dependsOn:
eval: $parent.contentType == 'infoBoard'
subFields:
- name: title
type: string
meta:
label: Titel
helperText: "Dieser Titel wird im Infobrett angezeigt."
- name: text
type: string
meta:
widget: richtext
label: Text
helperText: "Dieser Text wird im Infobrett angezeigt."
- name: icon
type: file
meta:
label: Icon
helperText: "Das Icon wird im Infobrett angezeigt."
- name: worldCard
type: object
meta:
label: Weltkarte
dependsOn:
eval: $parent.contentType == 'worldCard'
subFields:
- name: cards
type: object[]
meta:
label: Karten
subFields: !include cards.yml
- name: nestedCard
type: object[]
meta:
label: Verschatelte Karte
dependsOn:
eval: $parent.contentType == 'nestedCard'
subFields:
- name: title
type: string
meta:
label: Titel
helperText: "Dieser Titel wird in der äußeren Karte angezeigt."
- name: description
type: string
meta:
widget: richtext
label: Beschreibung
helperText: "Diese Beschreibung wird in der inneren Karte angezeigt."
- name: topDown
type: object
meta:
label: Top-Down
dependsOn:
eval: $parent.contentType == 'topDown'
subFields:
- name: rows
type: object[]
meta:
label: Zeilen
subFields:
- name: inital
type: string
meta:
label: Großbuchstabe
- name: rest
type: string
meta:
label: Rest
- name: description
type: string
meta:
label: Beschreibung
- name: personPreview
type: object
meta:
label: Personenvorschau
dependsOn:
eval: $parent.contentType == 'personPreview'
metaElements:
- initialImage
- hoverImage
subFields:
- name: initialImage
type: file
meta:
label: Bild
- name: hoverImage
type: file
meta:
label: Bild beim Hover
- name: name
type: string
meta:
label: Name
- name: boxList
type: object
meta:
label: Boxenliste
dependsOn:
eval: $parent.contentType == 'boxlist'
subFields:
- name: boxes
type: object[]
meta:
label: Boxen
subFields:
- name: name
type: string
meta:
label: Name
- name: extendableBox
type: object
meta:
label: Ausklappbare Box
dependsOn:
eval: $parent.contentType == 'extendableBox'
subFields:
- name: title
type: string
meta:
label: Titel
- name: text
type: string
meta:
widget: richtext
label: Text
- name: textLink
type: object
meta:
label: Text Link
dependsOn:
eval: $parent.contentType == 'textLink'
subFields:
- name: text
type: string
meta:
widget: richtext
label: Text
- name: link
type: string
meta:
label:
de: Seite
en: page
widget: select
choices:
endpoint: page
params:
sort: path
projection: navigation
mapping:
id: id
name: path

View File

@ -0,0 +1,11 @@
- name: boxes
type: object[]
meta:
label: Boxen
subFields: !include box.yml
- name: innerText
type: string
meta:
label: Innerer Text
helperText: "Dieser Text wird in der mitte vom Kreis angezeigt."

View File

@ -0,0 +1,5 @@
- name: boxes
type: object[]
meta:
label: Boxen
subFields: !include box.yml

View File

@ -0,0 +1,46 @@
name: row
type: object
meta:
label: Zeile
metaElements:
- topTitle
- title
- subTitle
- pageTitle
subFields:
- name: topTitle
type: string
meta:
label: Oberer Titel
helperText: "Dieser Titel wird in der Zeile oben angezeigt."
- name: title
type: string
meta:
label: Titel
helperText: "Dieser Titel wird in der Zeile angezeigt."
- name: subTitle
type: string
meta:
label: Untertitel
helperText: "Dieser Untertitel wird in der Zeile angezeigt."
- name: pageTitle
type: string
meta:
label: Titel der Seite
helperText: "Dieser Titel wird in der Seite als h1 angezeigt."
- name: backgroundImage
type: file
meta:
label: Hintergrundbild
helperText: "Dieses Bild wird als Hintergrundbild der Zeile angezeigt."
- name: columns
type: object[]
meta:
label: Spalten
direction: row
subFields: !include ../fieldLists/column.yml

View File

@ -0,0 +1,31 @@
name: teaser
type: object
meta:
label: Teaser
metaElements:
- showTeaser
subFields:
- name: showTeaser
type: boolean
meta:
label: Anzeigen
helperText: "Ist dies aktiviert, so wird der Teaser in der Startseite angezeigt."
- name: subTitle
type: string
meta:
label: Untertitel
helperText: "Dieser Untertitel wird in der Startseite angezeigt."
- name: teaserTitle
type: string
meta:
label: Titel
helperText: "Dieser Titel wird in der Startseite angezeigt."
- name: teaserDescription
type: string
meta:
widget: richtext
label: Beschreibung
helperText: "Diese Beschreibung wird in der Startseite angezeigt."

View File

@ -0,0 +1,82 @@
name: navigation
uploadPath: ../media/navigation
meta:
label: "Navigation"
muiIcon: navigation
views:
- type: simpleList
mediaQuery: "(max-width:599px)"
primaryText: tree
- type: table
mediaQuery: "(min-width:600px)"
columns:
- source: tree
permissions:
public:
methods:
get: true
post: false
put: false
delete: false
user:
methods:
get: true
post: false
put: true
delete: false
fields:
- name: tree
type: number
meta:
label: Baum
widget: select
helperText: Die Servicenavigation sollte Seiten wie bspw. die Datneschutzerklärung oder das Impressum umfassen.
choices:
- id: 0
name:
de: Hauptnavigation
en: main navigation
- id: 1
name:
de: Servicenavigation
en: service navigation
- name: pages
type: object[]
meta:
label:
de: Seiten
en: pages
folding:
previewUnfolded: name
previewFolded: name
subFields:
- name: name
type: string
meta:
label:
de: Name
en: name
helperText: Dieser Name wird zur Anzeige in der Navigation verwendet.
- name: page
type: string
meta:
label:
de: Seite
en: page
widget: select
choices:
endpoint: page
params:
sort: path
projection: navigation
mapping:
id: id
name: path

View File

@ -1,11 +1,11 @@
namespace: fontis
meta:
openapi:
servers:
- url: https://tibi-admin-server.code.testversion.online/api/v1/_/demo
description: code-server
collections: []
collections:
- !include collections/navigation.yml
- !include collections/content.yml

View File

@ -2,8 +2,12 @@
import Footer from "./lib/components/Footer.svelte"
import Header from "./lib/components/Menu/Header.svelte"
import Menu from "./lib/components/Menu/Menu.svelte"
import { location } from "./store"
import NotFound from "./lib/components/NotFound.svelte"
import Rows from "./lib/components/Pagebuilder/Rows.svelte"
import { location, navigation, pages, serviceNavigation } from "./lib/store"
import { Route, Router } from "svelte-routing"
import { loadPages } from "./lib/functions/getPages"
import { loadNavigation } from "./lib/functions/loadNavigation"
export let url = ""
if (url) {
@ -18,12 +22,45 @@
}
}
if (typeof window !== "undefined") console.log("App initialized")
let activeMenu = true
async function getPages() {
let pagesArray = await loadPages()
let pagesRes: Pages = {}
pagesArray.forEach((e) => {
pagesRes[e.path] = e
})
$pages = pagesRes
console.log(pagesRes)
}
async function getNavigation() {
let nav: Navigation[] = await loadNavigation()
console.log(nav)
$navigation = nav[0]
$serviceNavigation = nav[1]
}
getNavigation()
getPages()
let activeMenu = false
</script>
<main class="">
<Header bind:active="{activeMenu}" />
<div class="content-container" id="siteContainer" data-url="{url}">
<Router url="{url}">
<Route path="/">
<Rows path="/" homepage="{true}" />
</Route>
<Route path="/*path" let:params>
<Rows path="/{params?.path}" homepage="{false}" />
</Route>
<Route>
<NotFound />
</Route>
</Router>
</div>
<Footer />
</main>

121
frontend/src/api.ts Normal file
View File

@ -0,0 +1,121 @@
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
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
}
},
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
// @ts-ignore
return { data }
}

View File

@ -1,5 +1,5 @@
import App from "./App.svelte"
import { location } from "./store"
import { location } from "./lib/store"
const publishLocation = (_p?: string) => {
let _s: string

View File

@ -0,0 +1,52 @@
<script>
import { navigate } from "svelte-routing"
</script>
<div class="not-found">
<div class="content">
<h1>404</h1>
<h2>Seite nicht gefunden</h2>
<p>
Die gesuchte Seite wurde möglicherweise entfernt, ihr Name wurde geändert oder sie ist vorübergehend nicht
verfügbar.
</p>
<button on:click="{() => navigate('/')}" class="back-home">Zurück zur Startseite</button>
</div>
</div>
<style lang="less">
@import "../assets/css/main.less";
.not-found {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
.content {
text-align: center;
padding: 2rem;
background-color: rgba(255, 255, 255, 0.9);
h1 {
font-size: 6rem;
margin-bottom: 2rem;
}
h2 {
font-size: 2rem;
margin-bottom: 1rem;
}
p {
margin-bottom: 2rem;
}
.back-home {
text-decoration: none;
border: 1px solid black;
padding: 0.5rem 1rem;
transition: background-color 0.2s, color 0.2s;
}
}
}
</style>

View File

@ -0,0 +1,5 @@
<script lang="ts">
export let row: Row
</script>
<stlye lang="less"></stlye>

View File

@ -0,0 +1,8 @@
<script lang="ts">
export let row: Row
</script>
<div></div>
<style lang="less">
</style>

View File

@ -0,0 +1,27 @@
<script lang="ts">
import { init } from "svelte/internal"
import { pages } from "../../store"
export let path
export let homepage = false
let page: Page
function initPage() {
page = $pages[path]
}
$: {
if (Object.keys($pages).length) {
initPage()
}
}
</script>
<div>
{#if page}
{page.path}
{/if}
</div>
<style lang="less">
</style>

View File

@ -0,0 +1,6 @@
import { api } from "../../api"
export async function loadPages(): Promise<Page[]> {
let site = await api<Page[]>("page", {})
return site.data
}

View File

@ -0,0 +1,6 @@
import { api } from "../../api"
export async function loadNavigation(): Promise<Navigation[]> {
let nav = await api<Navigation[]>("navigation", {})
return nav.data
}

View File

@ -9,3 +9,7 @@ const initLoc = {
categoryPath: "",
}
export const location = writable(initLoc)
export let navigation = writable<Navigation>()
export let pages = writable<Pages>({})
export let serviceNavigation = writable<Navigation>()

View File

@ -43,7 +43,7 @@
"svelte-preprocess-esbuild": "^3.0.1",
"svelte-routing": "^1.6.0",
"tslib": "^2.5.0",
"typescript": "^5.0.4"
"typescript": "^4.9.5"
},
"dependencies": {
"@sentry/browser": "^7.48.0",

170
types/global.d.ts vendored
View File

@ -0,0 +1,170 @@
interface Pages {
[key: string]: Page
}
interface Page {
path: string
teaserHomepage: teaserHomepage
sectionHomepage: Row
rows: Row[]
id: string
}
interface teaserHomepage {
showTeaser: boolean
subtitle: string
title: string
description: string
}
interface Row {
topTitle: string
subTitle: string
title: string
pageTitle: string
columns: Column[]
backgroundImage: FileField
}
interface Column {
contentType:
| "image"
| "iconCycleSquare"
| "iconCycleCircle"
| "text"
| "infoBoard"
| "worldCard"
| "nestedCard"
| "topDown"
| "personPreview"
| "boxList"
| "extendableBox"
| "textLink"
| "iconBlock"
| "pageLinkBlocks"
| "publication"
image?: FileField
iconCycleSquare?: IconCycleSquare
iconCycleCircle?: IconCycleCircle
text: string
infoBoard: InfoBoard
worldCard: WorldCard
nestedCard: NestedCard
topDown: TopDown
personPreview: PersonPreview
boxList: BoxList
extendableBox: ExtendableBox
iconBlocks: iconBlock[]
pageLinkBlocks: pageLinkBlock[]
networkEvents: NetworkEvent[]
publication: Publication
}
interface Publication {
file: FileField
content: string
}
interface NetworkEvent {
beginnDate: Date
endDate: Date
title: string
file: FileField
}
interface iconBlock {
icon: FileField
bigText: string
smallText: string
}
interface pageLinkBlock {
name: string
rowNr: number
page: string
}
interface IconCycleSquare {
boxes: Box[]
}
interface IconCycleCircle {
boxes: Box[]
innerText: string
}
interface Box {
icon: FileField
text: string
circle: boolean
}
interface InfoBoard {
title: string
icon: FileField
text: string
}
interface WorldCard {
cards: Card[]
}
interface Card {
image: FileField
title: string
properties: number[]
}
interface NestedCard {
title: string
description: string
}
interface TopDown {
rows: TopDownRow[]
}
interface TopDownRow {
initial: string
rest: string
description: string
}
interface PersonPreview {
initalImage: FileField
hoverImage: FileField
name: string
}
interface BoxList {
names: {
name: string
}[]
}
interface ExtendableBox {
title: string
text: string
}
interface FileField {
path: string
src: string
type: string
}
interface TextLink {
text: string
link: string
}
interface Navigation {
tree: number
elemente: NavElement[]
}
interface NavElement {
name: string
page: string
}

View File

@ -4454,8 +4454,8 @@ __metadata:
linkType: hard
"node-fetch@npm:^2.6.7":
version: 2.6.9
resolution: "node-fetch@npm:2.6.9"
version: 2.6.12
resolution: "node-fetch@npm:2.6.12"
dependencies:
whatwg-url: ^5.0.0
peerDependencies:
@ -4463,7 +4463,7 @@ __metadata:
peerDependenciesMeta:
encoding:
optional: true
checksum: acb04f9ce7224965b2b59e71b33c639794d8991efd73855b0b250921382b38331ffc9d61bce502571f6cc6e11a8905ca9b1b6d4aeb586ab093e2756a1fd190d0
checksum: 3bc1655203d47ee8e313c0d96664b9673a3d4dd8002740318e9d27d14ef306693a4b2ef8d6525775056fd912a19e23f3ac0d7111ad8925877b7567b29a625592
languageName: node
linkType: hard
@ -5402,13 +5402,13 @@ __metadata:
linkType: hard
"postcss@npm:^8.4.23":
version: 8.4.25
resolution: "postcss@npm:8.4.25"
version: 8.4.26
resolution: "postcss@npm:8.4.26"
dependencies:
nanoid: ^3.3.6
picocolors: ^1.0.0
source-map-js: ^1.0.2
checksum: 9ed3ab8af43ad5210c28f56f916fd9b8c9f94fbeaebbf645dcf579bc28bdd8056c2a7ecc934668d399b81fedb6128f0c4b299f931e50454964bc911c25a3a0a2
checksum: 1cf08ee10d58cbe98f94bf12ac49a5e5ed1588507d333d2642aacc24369ca987274e1f60ff4cbf0081f70d2ab18a5cd3a4a273f188d835b8e7f3ba381b184e57
languageName: node
linkType: hard
@ -6315,7 +6315,7 @@ __metadata:
svelte-routing: ^1.6.0
swiper: ^9.2.0
tslib: ^2.5.0
typescript: ^5.0.4
typescript: ^4.9.5
languageName: unknown
linkType: soft
@ -6363,7 +6363,17 @@ __metadata:
languageName: node
linkType: hard
"typescript@npm:^5.0.3, typescript@npm:^5.0.4":
"typescript@npm:^4.9.5":
version: 4.9.5
resolution: "typescript@npm:4.9.5"
bin:
tsc: bin/tsc
tsserver: bin/tsserver
checksum: ee000bc26848147ad423b581bd250075662a354d84f0e06eb76d3b892328d8d4440b7487b5a83e851b12b255f55d71835b008a66cbf8f255a11e4400159237db
languageName: node
linkType: hard
"typescript@npm:^5.0.3":
version: 5.1.6
resolution: "typescript@npm:5.1.6"
bin:
@ -6373,7 +6383,17 @@ __metadata:
languageName: node
linkType: hard
"typescript@patch:typescript@^5.0.3#~builtin<compat/typescript>, typescript@patch:typescript@^5.0.4#~builtin<compat/typescript>":
"typescript@patch:typescript@^4.9.5#~builtin<compat/typescript>":
version: 4.9.5
resolution: "typescript@patch:typescript@npm%3A4.9.5#~builtin<compat/typescript>::version=4.9.5&hash=701156"
bin:
tsc: bin/tsc
tsserver: bin/tsserver
checksum: 2eee5c37cad4390385db5db5a8e81470e42e8f1401b0358d7390095d6f681b410f2c4a0c496c6ff9ebd775423c7785cdace7bcdad76c7bee283df3d9718c0f20
languageName: node
linkType: hard
"typescript@patch:typescript@^5.0.3#~builtin<compat/typescript>":
version: 5.1.6
resolution: "typescript@patch:typescript@npm%3A5.1.6#~builtin<compat/typescript>::version=5.1.6&hash=701156"
bin: