feat: added full indexeddb support

This commit is contained in:
Luke Hagar
2024-07-04 17:05:59 +00:00
parent 7f96b00b67
commit e23eae5f80
30 changed files with 979 additions and 796 deletions

View File

@@ -14,40 +14,40 @@
"devDependencies": { "devDependencies": {
"@skeletonlabs/skeleton": "2.10.0", "@skeletonlabs/skeleton": "2.10.0",
"@skeletonlabs/tw-plugin": "0.4.0", "@skeletonlabs/tw-plugin": "0.4.0",
"@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/adapter-auto": "^3.2.2",
"@sveltejs/kit": "^2.0.0", "@sveltejs/kit": "^2.5.18",
"@sveltejs/vite-plugin-svelte": "^3.0.0", "@sveltejs/vite-plugin-svelte": "^3.1.1",
"@tailwindcss/forms": "0.5.7", "@tailwindcss/forms": "0.5.7",
"@tailwindcss/typography": "0.5.13", "@tailwindcss/typography": "0.5.13",
"@types/eslint": "^8.56.0", "@types/eslint": "^8.56.10",
"@types/node": "20.12.12", "@types/node": "20.12.12",
"@typescript-eslint/eslint-plugin": "^7.0.0", "@typescript-eslint/eslint-plugin": "^7.15.0",
"@typescript-eslint/parser": "^7.0.0", "@typescript-eslint/parser": "^7.15.0",
"autoprefixer": "10.4.19", "autoprefixer": "10.4.19",
"dexie": "^4.0.7", "dexie": "^4.0.7",
"eslint": "^8.56.0", "eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.35.1", "eslint-plugin-svelte": "^2.41.0",
"filenamify": "^6.0.0", "filenamify": "^6.0.0",
"postcss": "8.4.38", "postcss": "8.4.38",
"prettier": "^3.1.1", "prettier": "^3.3.2",
"prettier-plugin-svelte": "^3.1.2", "prettier-plugin-svelte": "^3.2.5",
"spdx-license-list": "^6.9.0", "spdx-license-list": "^6.9.0",
"svelte": "^4.2.7", "svelte": "^4.2.18",
"svelte-check": "^3.6.0", "svelte-check": "^3.8.4",
"tailwindcss": "3.4.3", "tailwindcss": "3.4.3",
"tslib": "^2.4.1", "tslib": "^2.6.3",
"typescript": "^5.0.0", "typescript": "^5.5.3",
"vite": "^5.0.3", "vite": "^5.3.3",
"vite-plugin-tailwind-purgecss": "0.3.3", "vite-plugin-tailwind-purgecss": "0.3.3",
"yaml": "^2.4.2" "yaml": "^2.4.5"
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@floating-ui/dom": "1.6.5", "@floating-ui/dom": "1.6.5",
"@sveltejs/enhanced-img": "^0.2.0", "@sveltejs/enhanced-img": "^0.2.1",
"openapi-types": "^12.1.3", "openapi-types": "^12.1.3",
"svelte-persisted-store": "^0.9.2" "svelte-persisted-store": "^0.9.4"
}, },
"engines": { "engines": {
"node": ">20.0.0" "node": ">20.0.0"

790
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { newSpec, setSpec } from '$lib/db'; import { newSpec, loadSpec } from '$lib/db';
</script> </script>
<button <button
@@ -10,9 +10,9 @@
'This operation clears all the current values, unsaved data will be lost, are you sure?' 'This operation clears all the current values, unsaved data will be lost, are you sure?'
) )
) { ) {
setSpec(newSpec); loadSpec(structuredClone(newSpec));
} }
}} }}
> >
New Create New
</button> </button>

View File

@@ -1,14 +1,17 @@
<script lang="ts"> <script lang="ts">
import { newSpec } from '$lib/db'; import { newSpec } from '$lib/db';
import { db, setSpec, type APISpec } from '$lib/db'; import { db, loadSpec, type APISpec } from '$lib/db';
import type { CssClasses } from '@skeletonlabs/skeleton';
export let width: CssClasses = "w-full"
</script> </script>
<button <button
class="btn variant-ghost-error" class="btn variant-ghost-error {width}"
on:click={async () => { on:click={async () => {
if (confirm(`Are you sure you want to delete all saved specs?`)) { if (confirm(`Are you sure you want to delete all saved specs?`)) {
db.apiSpecs.clear(); db.apiSpecs.clear();
setSpec(newSpec); loadSpec(structuredClone(newSpec));
} }
}} }}
> >

View File

@@ -1,16 +1,24 @@
<script lang="ts"> <script lang="ts">
import { newSpec } from '$lib/db'; import { newSpec } from '$lib/db';
import { db, setSpec, type APISpec } from '$lib/db'; import { db, loadSpec, type APISpec } from '$lib/db';
import type { CssClasses } from '@skeletonlabs/skeleton';
export let spec: APISpec; export let spec: APISpec;
export let width: CssClasses = "w-full"
</script> </script>
<button <button
class="btn variant-ghost-error" class="btn variant-ghost-error {width}"
on:click={async () => { on:click={async () => {
if (confirm(`Are you sure you want to delete '${spec.name}'?`)) { if (confirm(`Are you sure you want to delete '${spec.name}'?`)) {
await db.apiSpecs.delete(spec.id); await db.apiSpecs.delete(spec.id);
setSpec(newSpec); const specs = await db.apiSpecs.toArray()
if (specs.length > 0){
loadSpec(specs[0]);
} else {
loadSpec(structuredClone(newSpec));
}
} }
}} }}
> >

View File

@@ -1,13 +1,13 @@
<script lang="ts"> <script lang="ts">
import { localStoragePrefix, openApiStore } from '$lib'; import { selectedSpec } from '$lib/db';
import filenamify from 'filenamify'; import filenamify from 'filenamify';
import { stringify } from 'yaml'; import { stringify } from 'yaml';
$: fileName = filenamify($openApiStore?.info?.title) || 'openapi'; $: fileName = filenamify($selectedSpec.spec?.info?.title) || 'openapi';
const saveYAML = () => { const saveYAML = () => {
if (!$openApiStore) return; if (!$selectedSpec.spec) return;
const openApi = $openApiStore; const openApi = $selectedSpec.spec;
const blob = new Blob([stringify(openApi, null, { indent: 2, aliasDuplicateObjects: false })], { const blob = new Blob([stringify(openApi, null, { indent: 2, aliasDuplicateObjects: false })], {
type: 'application/yaml' type: 'application/yaml'
}); });
@@ -21,8 +21,8 @@
}; };
const saveJSON = () => { const saveJSON = () => {
if (!$openApiStore) return; if (!$selectedSpec.spec) return;
const openApi = $openApiStore; const openApi = $selectedSpec.spec;
const blob = new Blob([JSON.stringify(openApi, null, 2)], { const blob = new Blob([JSON.stringify(openApi, null, 2)], {
type: 'application/json' type: 'application/json'
}); });

View File

@@ -1,12 +1,15 @@
<script lang="ts"> <script lang="ts">
import { setSpec, type APISpec } from '$lib/db'; import { loadSpec, type APISpec } from '$lib/db';
import type { CssClasses } from '@skeletonlabs/skeleton';
export let spec: APISpec; export let spec: APISpec;
export let width: CssClasses = "w-full"
function onLoad(e: Event): void { function onLoad(e: Event): void {
console.log('load button clicked'); console.log('load button clicked');
setSpec(spec); loadSpec(spec);
} }
</script> </script>
<button class="btn variant-ghost-warning" on:click={onLoad}> Load </button> <button class="btn variant-ghost-warning {width}" on:click={onLoad}> Load </button>

View File

@@ -1,18 +1,15 @@
<script lang="ts"> <script lang="ts">
import { openApiStore } from '$lib'; import { loadSpec, saveSpec, selectedSpec } from '$lib/db';
import { db, selectedSpec } from '$lib/db'; import type { CssClasses } from '@skeletonlabs/skeleton';
function onSave(e: Event): void { export let width: CssClasses = "w-full"
async function onSave(e: Event): Promise<void> {
console.log('Save button clicked'); console.log('Save button clicked');
if (!$selectedSpec) { const spec = await saveSpec($selectedSpec);
$selectedSpec = { if(spec) loadSpec(spec);
name: 'New OpenAPI Spec',
spec: $openApiStore
};
}
console.log($selectedSpec);
db.apiSpecs.put($selectedSpec, $selectedSpec.id);
} }
</script> </script>
<button class="btn variant-ghost-success" on:click={onSave}> Save </button> <button class="btn variant-ghost-success {width}" on:click={onSave}> Save </button>

View File

@@ -1,20 +1,12 @@
<script lang="ts"> <script lang="ts">
import { openApiStore } from '$lib';
import { db, selectedSpec } from '$lib/db'; import { db, selectedSpec } from '$lib/db';
function onSave(e: Event): void { function onSave(e: Event): void {
console.log('Save button clicked'); console.log('Save button clicked');
if (!$selectedSpec) {
$selectedSpec = {
name: 'New OpenAPI Spec',
spec: $openApiStore
};
}
console.log($selectedSpec);
const newSpec = structuredClone($selectedSpec); const newSpec = structuredClone($selectedSpec);
newSpec.id = undefined; newSpec.id = undefined;
db.apiSpecs.put(newSpec); db.apiSpecs.put(newSpec);
} }
</script> </script>
<button class="btn variant-ghost-success" on:click={onSave}> Save New </button> <button class="btn variant-ghost-success" on:click={onSave}> Save As </button>

View File

@@ -1,74 +0,0 @@
<script lang="ts">
import { openApiStore } from '$lib';
import { db, selectedSpec, setSpec } from '$lib/db';
import type { OpenAPIV3_1 } from '$lib/openAPITypes';
import {
FileButton,
FileDropzone,
getModalStore,
type ModalSettings,
type ModalStore
} from '@skeletonlabs/skeleton';
import { liveQuery } from 'dexie';
import { parse } from 'yaml';
let files: FileList | undefined;
function onFileUpload(e: Event): void {
console.log('onFileUpload');
if (!files) return;
const file = files[0];
const reader = new FileReader();
reader.onload = () => {
const result = reader.result as string;
const isJson = file.name.endsWith('.json');
let content: OpenAPIV3_1.Document;
try {
if (isJson) {
content = JSON.parse(result);
} else {
content = parse(result);
}
setSpec({ name: file.name, spec: content });
} catch (error) {
console.error(`Error parsing ${isJson ? 'json' : 'yaml'} file`, error);
}
};
reader.readAsText(file);
}
</script>
<FileDropzone
bind:files
label="upload"
accept=".yml,.yaml,.json"
on:dragover|once={() => {
files = undefined;
}}
on:change={onFileUpload}
type="file"
name="openapispec"
>
<svelte:fragment slot="lead">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-6 mx-auto"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m6.75 12-3-3m0 0-3 3m3-3v6m-1.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z"
/>
</svg>
</svelte:fragment>
<svelte:fragment slot="message">
<p class="mb-2 text-sm text-gray-500 dark:text-gray-400">
<span class="font-semibold">Click to upload</span> or drag and drop
</p>
</svelte:fragment>
<svelte:fragment slot="meta">JSON, YAML</svelte:fragment></FileDropzone
>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import type { ModalSettings } from '@skeletonlabs/skeleton';
import { getModalStore } from '@skeletonlabs/skeleton';
import type { CssClasses } from '@skeletonlabs/skeleton';
export let width: CssClasses = "w-full"
const modalStore = getModalStore();
const modal: ModalSettings = {
type: 'component',
component: 'uploadModal'
};
</script>
<button class="btn variant-ghost-tertiary {width}" on:click={() => modalStore.trigger(modal)}>Upload New</button>

View File

@@ -0,0 +1,138 @@
<script lang="ts">
import { pathCount, operationCount } from '$lib';
import { newSpec, saveSpec } from '$lib/db';
import { loadSpec, type APISpec } from '$lib/db';
import type { OpenAPIV3_1 } from '$lib/openAPITypes';
import { FileDropzone, ProgressRadial } from '@skeletonlabs/skeleton';
import { onMount, SvelteComponent } from 'svelte';
import { writable, type Writable } from 'svelte/store';
import { parse } from 'yaml';
// Props
/** Exposes parent props to this component. */
export let parent: SvelteComponent;
let files: FileList | undefined;
let uploadSpec: Writable<APISpec> = writable(newSpec);
let saving = false;
const extensionRegex = /\.(json|yml|yaml)$/;
$: stats = [
{
title: 'Paths',
value: pathCount($uploadSpec.spec)
},
{
title: 'Operation IDs',
value: operationCount($uploadSpec.spec)
}
];
function onFileUpload(e: Event): void {
console.log('onFileUpload');
if (!files) return;
const file = files[0];
const reader = new FileReader();
reader.onload = () => {
const result = reader.result as string;
const isJson = file.name.endsWith('.json');
let content: OpenAPIV3_1.Document;
try {
if (isJson) {
content = JSON.parse(result);
} else {
content = parse(result);
}
uploadSpec.set({ name: file.name.replace(extensionRegex, ''), spec: content });
} catch (error) {
console.error(`Error parsing ${isJson ? 'json' : 'yaml'} file`, error);
}
};
reader.readAsText(file);
}
</script>
<div class="flex flex-col gap-4 card p-4">
<label for="specName">
<span class="text-md font-bold">API Spec Name</span>
<input
class="input"
id="specName"
bind:value={$uploadSpec.name}
type="text"
placeholder="Enter the name for the API Spec"
/>
</label>
<div class="flex flex-col gap-1 justify-center">
{#each stats as stat}
<p class="">
{stat.title}: {stat.value}
</p>
{/each}
</div>
<label for="upload" class="block text-sm font-semibold text-token">
<span>Upload single file OpenAPI Specifications</span>
<FileDropzone
bind:files
label="upload"
accept=".yml,.yaml,.json"
on:dragover|once={() => {
files = undefined;
}}
on:change={onFileUpload}
single
type="file"
name="openapispec"
>
<svelte:fragment slot="lead">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-6 mx-auto text-token"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m6.75 12-3-3m0 0-3 3m3-3v6m-1.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z"
/>
</svg>
</svelte:fragment>
<svelte:fragment slot="message">
<p class="mb-2 text-sm text-token">
<span class="font-semibold">Click to upload</span> or drag and drop
</p>
</svelte:fragment>
<svelte:fragment slot="meta">
<p class="text-token">JSON, YAML</p></svelte:fragment
>
</FileDropzone>
</label>
<div class="flex flex-row gap-2">
<button
class="btn variant-ghost-success w-full"
on:click={async () => {
saving = true;
loadSpec($uploadSpec);
await saveSpec($uploadSpec);
parent.onClose();
}}
>
Save
</button>
<button class="btn variant-ghost-warning w-full" on:click={parent.onClose}
>{parent.buttonTextCancel}</button
>
</div>
<div class="size-6">
{#if saving}
<ProgressRadial width="size-6" />
{/if}
</div>
</div>

View File

@@ -15,7 +15,11 @@
class="select w-56 text-sm" class="select w-56 text-sm"
bind:value={$selectedSpec.spec.info.license.identifier} bind:value={$selectedSpec.spec.info.license.identifier}
on:change={() => { on:change={() => {
// @ts-expect-error - This is literally inside a null check
$selectedSpec.spec.info.license.url = null
// @ts-expect-error - This is only running on identifier change
$selectedSpec.spec.info.license.name = $selectedSpec.spec.info.license.name =
// @ts-expect-error - This is only running on identifier change
spdxLicenseList[$selectedSpec.spec.info.license.identifier].name; spdxLicenseList[$selectedSpec.spec.info.license.identifier].name;
}} }}
> >

View File

@@ -23,7 +23,7 @@
bind:value={flow.authorizationUrl} bind:value={flow.authorizationUrl}
/> />
</label> </label>
{#if type === 'authorizationCode'} {#if type === 'authorizationCode' && 'tokenUrl' in flow}
<label> <label>
<h5 class="h5">Token URL</h5> <h5 class="h5">Token URL</h5>
<p class="text-sm">The token URL to be used for this flow.</p> <p class="text-sm">The token URL to be used for this flow.</p>

View File

@@ -104,9 +104,12 @@
{#if multipleExamples} {#if multipleExamples}
<div class="space-y-2"> <div class="space-y-2">
<p>Examples</p> <p>Examples</p>
{#each Object.keys(value.examples) as example} {#if value.examples}
<ExampleInput bind:example={value.examples[example]} name={example} /> {#each Object.entries(value.examples) as example}
{/each} <ExampleInput bind:example={example[1]} name={example[0]} />
{/each}
{/if}
<button <button
type="button" type="button"
class="btn btn-sm variant-filled-primary" class="btn btn-sm variant-filled-primary"

View File

@@ -1,20 +1,25 @@
<script lang="ts"> <script lang="ts">
import type { Flows } from '$lib/types/auth'; import type { OpenAPIV3_1 } from '$lib/openAPITypes';
export let flow: Flows; export let flow: OpenAPIV3_1.OAuth2Scopes;
const addScope = () => { const addScope = () => {
flow.scopes = [...flow.scopes, [{ scope: '', description: '' }]]; // Correctly adds a new key with a string value to the scopes object
// @ts-expect-error - This is a valid operation
flow.scopes["newScope"] = "placeholder";
}; };
const removeScope = (index: number) => {
flow.scopes = flow.scopes.filter((_, i) => i !== index); const removeScope = (key: string) => {
// To remove a key from an object, use the delete operator instead of setting it to undefined
//@ts-expect-error - This is a valid operation
delete flow.scopes[key];
}; };
</script> </script>
<h4 class="h4">Scopes</h4> <h4 class="h4">Scopes</h4>
<table class="table"> <table class="table">
<tbody> <tbody>
{#each flow.scopes as scope, index} {#each Object.entries(flow.scopes) as scope, index}
{#each scope as item, i} {#each scope as item, i}
<tr> <tr>
<td class="px-4"> <td class="px-4">
@@ -23,16 +28,7 @@
name="scope{index}-{i}" name="scope{index}-{i}"
class="input" class="input"
placeholder="scope" placeholder="scope"
bind:value={item.scope} bind:value={item}
/>
</td>
<td class="px-2">
<input
type="text"
name="description{index}-{i}"
class="input"
placeholder="a short description of the scope (optional)"
bind:value={item.description}
/> />
</td> </td>
<td class="max-w-16 px-2"> <td class="max-w-16 px-2">
@@ -40,7 +36,7 @@
type="button" type="button"
class="btn variant-ringed-error hover:variant-filled-error" class="btn variant-ringed-error hover:variant-filled-error"
on:click={() => { on:click={() => {
removeScope(index); removeScope(item);
}} }}
> >
Remove Scope Remove Scope

View File

@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { openApiStore } from '$lib';
import { import {
apiKeyAuthTemplate, apiKeyAuthTemplate,
basicAuthTemplate, basicAuthTemplate,
@@ -7,11 +6,12 @@
oauth2AuthTemplate, oauth2AuthTemplate,
openIdAuthTemplate openIdAuthTemplate
} from '$lib/authTemplates'; } from '$lib/authTemplates';
import { selectedSpec } from '$lib/db';
import AuthenticationItem from '../atoms/AuthenticationItem.svelte'; import AuthenticationItem from '../atoms/AuthenticationItem.svelte';
let selectedSchema: string; let selectedSchema: string;
const addSecuritySchema = () => { const addSecuritySchema = () => {
let tempSchemaList = $openApiStore.security; let tempSchemaList = $selectedSpec.spec.security || [];
let newSchema; let newSchema;
switch (selectedSchema) { switch (selectedSchema) {
case 'basicAuth': case 'basicAuth':
@@ -36,47 +36,61 @@
if (newSchema) { if (newSchema) {
tempSchemaList = [...tempSchemaList, newSchema]; tempSchemaList = [...tempSchemaList, newSchema];
$openApiStore.security = tempSchemaList; $selectedSpec.spec.security = tempSchemaList;
} }
}; };
const removeSecuritySchema = (index: number) => { const removeSecuritySchema = (index: number) => {
let tempSchemaList = $openApiStore.security; let tempSchemaList = $selectedSpec.spec.security;
tempSchemaList.splice(index, 1); tempSchemaList.splice(index, 1);
$openApiStore.security = tempSchemaList; $selectedSpec.spec.security = tempSchemaList;
}; };
</script> </script>
<form {#if $selectedSpec.spec.security}
class="border-token rounded-container-token bg-surface-backdrop-token px-6 py-4 min-h-20 space-y-4" <form
> class="border-token rounded-container-token bg-surface-backdrop-token px-6 py-4 min-h-20 space-y-4"
{#each $openApiStore.security as schema, index} >
<div class="card w-full p-4"> {#each $selectedSpec.spec.security as schema, index}
<div class="flex flex-row-reverse w-full"> <div class="card w-full p-4">
<button <div class="flex flex-row-reverse w-full">
type="button" <button
class="btn btn-sm variant-ringed-error hover:variant-filled-error" type="button"
on:click={() => { class="btn btn-sm variant-ringed-error hover:variant-filled-error"
removeSecuritySchema(index); on:click={() => {
}} removeSecuritySchema(index);
> }}
Remove schema >
</button> Remove schema
</button>
</div>
<AuthenticationItem bind:schema />
</div> </div>
<AuthenticationItem bind:schema /> <hr />
</div> {/each}
<hr />
{/each}
<span class="flex justify-center items-center gap-2 max-w-sm mx-auto"> <span class="flex justify-center items-center gap-2 max-w-sm mx-auto">
<select name="security-schema" bind:value={selectedSchema} class="input w-fit text-sm"> <select name="security-schema" bind:value={selectedSchema} class="input w-fit text-sm">
<option value="basicAuth" selected>Basic Auth</option> <option value="basicAuth" selected>Basic Auth</option>
<option value="bearerAuth">Bearer Auth</option> <option value="bearerAuth">Bearer Auth</option>
<option value="ApiKeyAuth">API Key Auth</option> <option value="ApiKeyAuth">API Key Auth</option>
<option value="openId">OpenID</option> <option value="openId">OpenID</option>
<option value="oAuthSample">OAuth2</option> <option value="oAuthSample">OAuth2</option>
</select> </select>
<button type="button" class="btn text-sm variant-filled-primary" on:click={addSecuritySchema}> <button type="button" class="btn text-sm variant-filled-primary" on:click={addSecuritySchema}>
Add Security Schema Add Security Schema
</button>
</span>
</form>
{:else}
<div class="grid place-content-center h-full">
<p class="p-4">No security schema defined</p>
<button
class="btn variant-ghost-success"
on:click={() => {
addSecuritySchema();
}}
>
Add security schema
</button> </button>
</span> </div>
</form> {/if}

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import Info from '../icons/Info.svelte'; import Info from '../icons/Info.svelte';
import LicenseAtom from '../atoms/LicenseAtom.svelte'; import LicenseAtom from '../atoms/LicenseAtom.svelte';
import { openApiStore } from '$lib'; import { selectedSpec } from '$lib/db';
</script> </script>
<form class="space-y-2"> <form class="space-y-2">
@@ -13,7 +13,7 @@
name="title" name="title"
placeholder="Sample API" placeholder="Sample API"
type="text" type="text"
bind:value={$openApiStore.info.title} bind:value={$selectedSpec.spec.info.title}
required required
/> />
</label> </label>
@@ -23,7 +23,7 @@
class="textarea" class="textarea"
name="description" name="description"
placeholder="Optional multiline or single-line description. Supports Markdown." placeholder="Optional multiline or single-line description. Supports Markdown."
bind:value={$openApiStore.info.description} bind:value={$selectedSpec.spec.info.description}
/> />
</label> </label>
<label class="space-y-1"> <label class="space-y-1">
@@ -33,7 +33,7 @@
name="version" name="version"
placeholder="0.1.0" placeholder="0.1.0"
type="text" type="text"
bind:value={$openApiStore.info.version} bind:value={$selectedSpec.spec.info.version}
required required
/> />
</label> </label>
@@ -44,13 +44,13 @@
name="termsOfService" name="termsOfService"
placeholder="https://example.com/terms" placeholder="https://example.com/terms"
type="url" type="url"
bind:value={$openApiStore.info.termsOfService} bind:value={$selectedSpec.spec.info.termsOfService}
/> />
</label> </label>
</div> </div>
<div class="border-token rounded-container-token bg-surface-backdrop-token space-y-1 p-4"> <div class="border-token rounded-container-token bg-surface-backdrop-token space-y-1 p-4">
<h4 class="h4">Contact Information</h4> <h4 class="h4">Contact Information</h4>
{#if $openApiStore.info.contact} {#if $selectedSpec.spec.info.contact}
<label class="space-y-1"> <label class="space-y-1">
<span class="text-sm">Name (optional)</span> <span class="text-sm">Name (optional)</span>
<input <input
@@ -58,7 +58,7 @@
name="contactName" name="contactName"
placeholder="John Doe" placeholder="John Doe"
type="text" type="text"
bind:value={$openApiStore.info.contact.name} bind:value={$selectedSpec.spec.info.contact.name}
/> />
</label> </label>
<label class="space-y-1"> <label class="space-y-1">
@@ -68,7 +68,7 @@
name="contactEmail" name="contactEmail"
placeholder="email@example.com" placeholder="email@example.com"
type="email" type="email"
bind:value={$openApiStore.info.contact.email} bind:value={$selectedSpec.spec.info.contact.email}
/> />
</label> </label>
<label class="space-y-1"> <label class="space-y-1">
@@ -78,7 +78,7 @@
name="contactUrl" name="contactUrl"
placeholder="https://example.com" placeholder="https://example.com"
type="url" type="url"
bind:value={$openApiStore.info.contact.url} bind:value={$selectedSpec.spec.info.contact.url}
/> />
</label> </label>
{:else} {:else}
@@ -86,7 +86,7 @@
type="button" type="button"
class="btn variant-filled-primary" class="btn variant-filled-primary"
on:click={() => { on:click={() => {
$openApiStore.info.contact = { $selectedSpec.spec.info.contact = {
name: '', name: '',
email: '', email: '',
url: '' url: ''

View File

@@ -1,13 +1,17 @@
<script lang="ts"> <script lang="ts">
import { addPath } from '$lib'; import { addPath } from '$lib';
import { openApiStore, sortPathsAlphabetically } from '$lib'; import { sortPathsAlphabetically } from '$lib';
import type { OpenAPIV3 } from '$lib/openAPITypes'; import { selectedSpec } from '$lib/db';
import type { OpenAPIV3_1 } from '$lib/openAPITypes';
import PathListItem from '../atoms/PathListItem.svelte'; import PathListItem from '../atoms/PathListItem.svelte';
import { getModalStore } from '@skeletonlabs/skeleton'; import { getModalStore } from '@skeletonlabs/skeleton';
let paths: OpenAPIV3.PathsObject = {}; let paths: OpenAPIV3_1.PathsObject = {};
// @ts-expect-error - working with a potentially empty object selectedSpec.subscribe((store) => {
openApiStore.subscribe((store) => (paths = store.paths)); if (store.spec.paths) {
paths = store.spec.paths;
}
});
const modalStore = getModalStore(); const modalStore = getModalStore();
</script> </script>

View File

@@ -1,35 +1,35 @@
<script lang="ts"> <script lang="ts">
import { openApiStore } from '$lib'; import { selectedSpec } from '$lib/db';
import ServerInput from '../atoms/ServerInput.svelte'; import ServerInput from '../atoms/ServerInput.svelte';
const addServer = () => { const addServer = () => {
// check if the servers array is not undefined // check if the servers array is not undefined
if (!$openApiStore.servers) { if (!$selectedSpec.spec.servers) {
$openApiStore.servers = []; $selectedSpec.spec.servers = [];
} }
let tempServers = [...$openApiStore.servers]; let tempServers = [...$selectedSpec.spec.servers];
tempServers.push({ url: '', description: '' }); tempServers.push({ url: '', description: '' });
$openApiStore.servers = tempServers; $selectedSpec.spec.servers = tempServers;
}; };
const removeServer = (index: number) => { const removeServer = (index: number) => {
// check if the servers array is not undefined // check if the servers array is not undefined
if (!$openApiStore.servers) { if (!$selectedSpec.spec.servers) {
$openApiStore.servers = []; $selectedSpec.spec.servers = [];
} }
let tempServers = [...$openApiStore.servers]; let tempServers = [...$selectedSpec.spec.servers];
tempServers.splice(index, 1); tempServers.splice(index, 1);
$openApiStore.servers = tempServers; $selectedSpec.spec.servers = tempServers;
}; };
</script> </script>
<form <form
class=" mx-auto border-token rounded-container-token bg-surface-backdrop-token p-4 min-h-20 space-y-6" class=" mx-auto border-token rounded-container-token bg-surface-backdrop-token p-4 min-h-20 space-y-6"
> >
<!-- If openApiStore.servers isnt an array show add button --> <!-- If selectedSpec$selectedSpec.spec.servers isnt an array show add button -->
{#if Array.isArray($openApiStore.servers)} {#if Array.isArray($selectedSpec.spec.servers)}
<ul class="space-y-6"> <ul class="space-y-6">
{#each $openApiStore.servers as server, index} {#each $selectedSpec.spec.servers as server, index}
<li class="!block"> <li class="!block">
<span class="flex w-full justify-between"> <span class="flex w-full justify-between">
<span>Server {index + 1} </span> <span>Server {index + 1} </span>
@@ -43,12 +43,12 @@
</span> </span>
<ServerInput id={1} bind:server /> <ServerInput id={1} bind:server />
</li> </li>
{#if index < $openApiStore.servers.length - 1} {#if index < $selectedSpec.spec.servers.length - 1}
<hr /> <hr />
{/if} {/if}
{/each} {/each}
</ul> </ul>
<span class="flex justify-center" class:!mt-0={$openApiStore.servers.length === 0}> <span class="flex justify-center" class:!mt-0={$selectedSpec.spec.servers.length === 0}>
<button type="button" class="btn text-sm variant-filled-primary" on:click={addServer}> <button type="button" class="btn text-sm variant-filled-primary" on:click={addServer}>
Add Server Add Server
</button> </button>

View File

@@ -1,15 +1,63 @@
import { blankSpec } from "$lib";
import type { OpenAPIV3_1 } from "$lib/openAPITypes"; import type { OpenAPIV3_1 } from "$lib/openAPITypes";
import Dexie, { type Table } from 'dexie'; import Dexie, { type Table } from 'dexie';
import { persisted } from "svelte-persisted-store";
import { writable, type Writable } from "svelte/store"; import { writable, type Writable } from "svelte/store";
export const blankSpec: OpenAPIV3_1.Document = {
openapi: '3.1.0', // OpenAPI version
jsonSchemaDialect: 'https://json-schema.org/draft/2020-12/schema', // JSON Schema version
info: {
/** Title of the API (required) */
title: '',
/** Description of the API (optional) */
description: '',
/** Terms of service link (optional) */
termsOfService: '',
/** API Version (required) */
version: '',
/** Contact Information */
contact: {
/** Name of the contact person/organization. */
name: '', // optional
/** URL pointing to the contact information. MUST be in the format of a URL. */
url: '', // optional
/** Email address of the contact person/organization. MUST be in the format of an email address. */
email: '' // optional
},
license: {
name: '', // required if license is included
url: '' // optional
}
},
servers: [],
paths: {},
webhooks: {},
components: {},
security: [],
tags: [],
externalDocs: {
description: '',
url: ''
}
};
export const newSpec: APISpec = { export const newSpec: APISpec = {
name: 'OpenAPI', name: 'OpenAPI',
spec: blankSpec spec: blankSpec
} } as const
export const selectedSpecId: Writable<string | undefined> = persisted("selectedSpecId",undefined)
export const selectedSpec: Writable<APISpec> = writable(newSpec) export const selectedSpec: Writable<APISpec> = writable(newSpec)
selectedSpec.subscribe((spec) => {
if(!spec){
spec = structuredClone(newSpec)
}
if(spec.id){
selectedSpecId.set(spec.id)
}
})
export interface APISpec { export interface APISpec {
id?: string; id?: string;
name: string; name: string;
@@ -28,3 +76,19 @@ export class MySubClassedDexie extends Dexie {
} }
export const db = new MySubClassedDexie(); export const db = new MySubClassedDexie();
export const loadSpec = (spec: APISpec) => {
selectedSpec.set(spec)
selectedSpecId.set(spec.id!)
}
export const saveSpec = async (spec: APISpec) => {
const clonedSpec = structuredClone(spec)
let specID
if (clonedSpec.id) {
specID = await db.apiSpecs.put(clonedSpec, clonedSpec.id)
} else {
specID = await db.apiSpecs.add(clonedSpec)
}
return await db.apiSpecs.get(specID)
}

View File

@@ -1,46 +1,8 @@
import type { OpenAPIV3_1 } from 'openapi-types'; import type { OpenAPIV3_1 } from './openAPITypes';
import { writable, type Writable } from 'svelte/store'; import { writable, type Writable } from 'svelte/store';
export const localStoragePrefix = 'openapigen-'; export const localStoragePrefix = 'openapigen-';
export const blankSpec: OpenAPIV3_1.Document = {
openapi: '3.1.0', // OpenAPI version
jsonSchemaDialect: 'https://json-schema.org/draft/2020-12/schema', // JSON Schema version
info: {
/** Title of the API (required) */
title: '',
/** Description of the API (optional) */
description: '',
/** Terms of service link (optional) */
termsOfService: '',
/** API Version (required) */
version: '',
/** Contact Information */
contact: {
/** Name of the contact person/organization. */
name: '', // optional
/** URL pointing to the contact information. MUST be in the format of a URL. */
url: '', // optional
/** Email address of the contact person/organization. MUST be in the format of an email address. */
email: '' // optional
},
license: {
name: '', // required if license is included
url: '' // optional
}
},
servers: [],
paths: {},
webhooks: {},
components: {},
security: [],
tags: [],
externalDocs: {
description: '',
url: ''
}
};
export const operationCount = (openApiDoc: OpenAPIV3_1.Document) => { export const operationCount = (openApiDoc: OpenAPIV3_1.Document) => {
let count = 0; let count = 0;
for (const path in openApiDoc.paths) { for (const path in openApiDoc.paths) {

View File

@@ -15,13 +15,14 @@ export declare namespace OpenAPIV3_1 {
info: InfoObject; info: InfoObject;
jsonSchemaDialect?: string; jsonSchemaDialect?: string;
servers?: ServerObject[]; servers?: ServerObject[];
security?: SecurityRequirementObject[];
} & ( } & (
| (Pick<PathsWebhooksComponents<T>, 'paths'> & | (Pick<PathsWebhooksComponents<T>, 'paths'> &
Omit<Partial<PathsWebhooksComponents<T>>, 'paths'>) Omit<Partial<PathsWebhooksComponents<T>>, 'paths'>)
| (Pick<PathsWebhooksComponents<T>, 'webhooks'> & | (Pick<PathsWebhooksComponents<T>, 'webhooks'> &
Omit<Partial<PathsWebhooksComponents<T>>, 'webhooks'>) Omit<Partial<PathsWebhooksComponents<T>>, 'webhooks'>)
| (Pick<PathsWebhooksComponents<T>, 'components'> & | (Pick<PathsWebhooksComponents<T>, 'components'> &
Omit<Partial<PathsWebhooksComponents<T>>, 'components'>) Omit<Partial<PathsWebhooksComponents<T>>, 'components'>)
) )
>; >;
export type InfoObject = Modify< export type InfoObject = Modify<
@@ -64,8 +65,8 @@ export declare namespace OpenAPIV3_1 {
parameters?: (ReferenceObject | ParameterObject)[]; parameters?: (ReferenceObject | ParameterObject)[];
} }
> & { > & {
[method in HttpMethods]?: OperationObject<T>; [method in HttpMethods]?: OperationObject<T>;
}; };
export type OperationObject<T extends {} = {}> = Modify< export type OperationObject<T extends {} = {}> = Modify<
OpenAPIV3.OperationObject<T>, OpenAPIV3.OperationObject<T>,
{ {
@@ -192,7 +193,9 @@ export declare namespace OpenAPIV3_1 {
export type OAuth2SecurityScheme = OpenAPIV3.OAuth2SecurityScheme; export type OAuth2SecurityScheme = OpenAPIV3.OAuth2SecurityScheme;
export type OpenIdSecurityScheme = OpenAPIV3.OpenIdSecurityScheme; export type OpenIdSecurityScheme = OpenAPIV3.OpenIdSecurityScheme;
export type TagObject = OpenAPIV3.TagObject; export type TagObject = OpenAPIV3.TagObject;
export {}; export type OAuth2Flows = OpenAPIV3.OAuth2Flows;
export type OAuth2Scopes = OpenAPIV3.OAuth2Scopes;
export { };
} }
export declare namespace OpenAPIV3 { export declare namespace OpenAPIV3 {
@@ -267,8 +270,8 @@ export declare namespace OpenAPIV3 {
servers?: ServerObject[]; servers?: ServerObject[];
parameters?: (ReferenceObject | ParameterObject)[]; parameters?: (ReferenceObject | ParameterObject)[];
} & { } & {
[method in HttpMethods]?: OperationObject<T>; [method in HttpMethods]?: OperationObject<T>;
}; };
type OperationObject<T extends {} = {}> = { type OperationObject<T extends {} = {}> = {
tags?: string[]; tags?: string[];
summary?: string; summary?: string;
@@ -293,7 +296,7 @@ export declare namespace OpenAPIV3 {
name: string; name: string;
in: string; in: string;
} }
interface HeaderObject extends ParameterBaseObject {} interface HeaderObject extends ParameterBaseObject { }
interface ParameterBaseObject { interface ParameterBaseObject {
description?: string; description?: string;
required?: boolean; required?: boolean;
@@ -483,40 +486,38 @@ export declare namespace OpenAPIV3 {
name: string; name: string;
in: string; in: string;
} }
interface OAuth2Scopes {
[scope: string]: string;
}
interface OAuth2Flows {
implicit?: {
authorizationUrl: string;
refreshUrl?: string;
scopes: OAuth2Scopes
};
password?: {
tokenUrl: string;
refreshUrl?: string;
scopes: OAuth2Scopes
};
clientCredentials?: {
tokenUrl: string;
refreshUrl?: string;
scopes: OAuth2Scopes
};
authorizationCode?: {
authorizationUrl: string;
tokenUrl: string;
refreshUrl?: string;
scopes: OAuth2Scopes
};
}
interface OAuth2SecurityScheme { interface OAuth2SecurityScheme {
type: 'oauth2'; type: 'oauth2';
description?: string; description?: string;
flows: { flows: OAuth2Flows;
implicit?: {
authorizationUrl: string;
refreshUrl?: string;
scopes: {
[scope: string]: string;
};
};
password?: {
tokenUrl: string;
refreshUrl?: string;
scopes: {
[scope: string]: string;
};
};
clientCredentials?: {
tokenUrl: string;
refreshUrl?: string;
scopes: {
[scope: string]: string;
};
};
authorizationCode?: {
authorizationUrl: string;
tokenUrl: string;
refreshUrl?: string;
scopes: {
[scope: string]: string;
};
};
};
} }
interface OpenIdSecurityScheme { interface OpenIdSecurityScheme {
type: 'openIdConnect'; type: 'openIdConnect';

View File

@@ -139,7 +139,7 @@ export const isValidPath = (path: string) => {
// check if path is a valid path // check if path is a valid path
const pathWithoutVariables = path.replaceAll('{', '').replaceAll('}', ''); const pathWithoutVariables = path.replaceAll('{', '').replaceAll('}', '');
console.log(pathWithoutVariables); console.log(pathWithoutVariables);
const pathRegex = /(\/[a-zA-Z-]+)+|\//gm; const pathRegex = /(\/[a-zA-Z0-9]+)+|\//gm;
const pathParts = pathWithoutVariables.match(pathRegex); const pathParts = pathWithoutVariables.match(pathRegex);
// the fallback is to return false if the pathParts array is null // the fallback is to return false if the pathParts array is null
return pathParts?.length === 1; return pathParts?.length === 1;
@@ -150,11 +150,11 @@ export const sortPathsAlphabetically = () => {
const tempPathObject = {}; const tempPathObject = {};
const store = get(selectedSpec); const store = get(selectedSpec);
// @ts-expect-error - we are working with an initially empty object // @ts-expect-error - we are working with an initially empty object
Object.keys(store.paths) Object.keys(store.spec.paths)
.sort() .sort()
.forEach((key) => { .forEach((key) => {
// @ts-expect-error - we are working with initially empty objects // @ts-expect-error - we are working with initially empty objects
tempPathObject[key] = store.paths[key]; tempPathObject[key] = store.spec.paths[key];
}); });
// update path object // update path object

View File

@@ -1,21 +1,25 @@
<script lang="ts"> <script lang="ts">
import FancyAppRail from './FancyAppRail.svelte'; import FancyAppRail from './FancyAppRail.svelte';
import { AppShell, type ModalComponent } from '@skeletonlabs/skeleton';
import '../app.postcss'; import '../app.postcss';
import { AppBar, AppShell } from '@skeletonlabs/skeleton'; // Floating UI for Popups
import { arrow, autoUpdate, computePosition, flip, offset, shift } from '@floating-ui/dom';
// Floating UI for Popups
import { computePosition, autoUpdate, flip, shift, offset, arrow } from '@floating-ui/dom';
import { storePopup } from '@skeletonlabs/skeleton'; import { storePopup } from '@skeletonlabs/skeleton';
import { localStoragePrefix } from '$lib'; // Modal
storePopup.set({ computePosition, autoUpdate, flip, shift, offset, arrow }); import { initializeStores, Modal } from '@skeletonlabs/skeleton';
import UploadModal from '$lib/components/FileManagement/UploadModal.svelte';
// Modal
import { initializeStores, Modal, LightSwitch } from '@skeletonlabs/skeleton';
initializeStores(); initializeStores();
const components: Record<string, ModalComponent> = {
// Set a unique modal ID, then pass the component reference
uploadModal: { ref: UploadModal },
// ...
};
</script> </script>
<Modal /> <Modal {components} />
<AppShell slotPageContent="px-6 py-4"> <AppShell slotPageContent="px-6 py-4">
<svelte:fragment slot="sidebarLeft"> <svelte:fragment slot="sidebarLeft">

View File

@@ -1,60 +1,120 @@
<script lang="ts"> <script lang="ts">
import { pathCount, operationCount } from '$lib';
import CreateNewButton from '$lib/components/FileManagement/CreateNewButton.svelte'; import CreateNewButton from '$lib/components/FileManagement/CreateNewButton.svelte';
import DeleteAllButton from '$lib/components/FileManagement/DeleteAllButton.svelte'; import DeleteAllButton from '$lib/components/FileManagement/DeleteAllButton.svelte';
import DeleteButton from '$lib/components/FileManagement/DeleteButton.svelte'; import DeleteButton from '$lib/components/FileManagement/DeleteButton.svelte';
import LoadButton from '$lib/components/FileManagement/LoadButton.svelte'; import LoadButton from '$lib/components/FileManagement/LoadButton.svelte';
import SaveButton from '$lib/components/FileManagement/SaveButton.svelte'; import SaveButton from '$lib/components/FileManagement/SaveButton.svelte';
import SaveNewButton from '$lib/components/FileManagement/SaveNewButton.svelte'; import SaveNewButton from '$lib/components/FileManagement/SaveNewButton.svelte';
import Upload from '$lib/components/FileManagement/Upload.svelte'; import UploadButton from '$lib/components/FileManagement/UploadButton.svelte';
import { db, selectedSpec, setSpec } from '$lib/db'; import { db, loadSpec, newSpec, selectedSpec, selectedSpecId } from '$lib/db';
import { liveQuery } from 'dexie'; import { liveQuery } from 'dexie';
import { ProgressRadial } from '@skeletonlabs/skeleton';
import { onMount } from 'svelte';
let apiSpecs = liveQuery(() => db.apiSpecs.toArray()); let apiSpecs = liveQuery(() => db.apiSpecs.toArray());
let specLoaded = false;
let pageLoaded = false;
let loaded = false;
apiSpecs.subscribe((specs) => { apiSpecs.subscribe((specs) => {
if (loaded) return; if (specLoaded) return;
if (specs.length > 0) {
setSpec(specs[0]); if ($selectedSpecId !== $selectedSpec.id) {
const found = specs.find((spec) => spec.id === $selectedSpecId);
if (found) {
loadSpec(found);
specLoaded = true;
}
} else if (specs.length > 0) {
loadSpec(specs[0]);
specLoaded = true;
} }
}); });
$: console.log($apiSpecs);
$: stats = [
{
title: 'Paths',
value: pathCount($selectedSpec.spec)
},
{
title: 'Operation IDs',
value: operationCount($selectedSpec.spec)
}
];
onMount(() => {
console.log('onMount', $selectedSpecId, $selectedSpec);
if ($selectedSpec) {
pageLoaded = true;
}
});
$: console.log('newSpec', newSpec, $selectedSpec, $selectedSpecId, $apiSpecs, $apiSpecs?.length, specLoaded);
</script> </script>
<div class="grid place-content-center h-full gap-2 px-1"> <div class="grid place-content-center h-full gap-2 px-1">
<table class="table"> {#if !specLoaded && !pageLoaded}
<thead> <ProgressRadial />
<tr> {:else}
<th class="text-left">Name</th> <div class="card flex flex-col gap-4 p-4">
<th class="text-center">Actions</th> <p class="text-lg font-bold">Selected Spec</p>
</tr> <label class="text-lg">
</thead> <span class="font-semibold">Name:</span>
<tbody> <input type="text" bind:value={$selectedSpec.name} class="input w-full" />
{#if $apiSpecs} </label>
{#each $apiSpecs as spec (spec.id)} <div>
<tr> <p><span class="font-semibold text-lg">Id:</span> {$selectedSpec.id || 'Not Saved'}</p>
<td>{spec.name}</td> </div>
<td> <div>
<LoadButton {spec} /> <p class="font-semibold text-lg">Stats:</p>
<DeleteButton {spec} /> {#each stats as stat}
</td> <p class="">
</tr> {stat.title}: {stat.value}
</p>
{/each} {/each}
{/if} </div>
</tbody> <div class="flex flex-row justify-center gap-2">
</table> {#if $selectedSpec.id}
{#if $selectedSpec} <CreateNewButton />
<input {/if}
class="input" <SaveButton />
bind:value={$selectedSpec.name} {#if $selectedSpec.id}
type="text" <SaveNewButton />
placeholder="Enter the name for the API Spec" <DeleteButton spec={$selectedSpec} />
/> {/if}
</div>
</div>
{#if $apiSpecs && $apiSpecs.length > 0}
<table class="table">
<thead>
<tr>
<th class="text-left">Name</th>
<th class="text-center">Actions</th>
</tr>
</thead>
<tbody>
{#if $apiSpecs}
{#each $apiSpecs as spec (spec.id)}
<tr>
<td class="!align-middle">
<span class="font-bold">{spec.name}</span>
</td>
<td class="flex flex-row justify-center gap-2">
<LoadButton {spec} />
<DeleteButton {spec} />
</td>
</tr>
{/each}
{/if}
</tbody>
</table>
<div class="flex flex-row justify-center w-full gap-4">
<DeleteAllButton />
<UploadButton />
</div>
{:else}
<UploadButton />
{/if}
{/if} {/if}
<SaveButton />
<SaveNewButton />
<CreateNewButton />
<Upload />
<DeleteAllButton />
</div> </div>

View File

@@ -1,12 +1,7 @@
<script lang="ts"> <script lang="ts">
import { AppRail, AppRailAnchor, LightSwitch } from '@skeletonlabs/skeleton';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { liveQuery } from 'dexie';
import { db } from '$lib/db';
import DownloadButtons from '$lib/components/FileManagement/DownloadButtons.svelte'; import DownloadButtons from '$lib/components/FileManagement/DownloadButtons.svelte';
import { AppRail, AppRailAnchor, LightSwitch } from '@skeletonlabs/skeleton';
let apiSpecs = liveQuery(() => db.apiSpecs.toArray());
$: console.log($apiSpecs);
</script> </script>
<AppRail <AppRail
@@ -133,23 +128,7 @@
</svg> </svg>
Components Components
</AppRailAnchor> </AppRailAnchor>
<AppRailAnchor href="/stats" selected={$page.url.pathname === '/stats'}>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-6 mx-auto"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M7.5 14.25v2.25m3-4.5v4.5m3-6.75v6.75m3-9v9M6 20.25h12A2.25 2.25 0 0 0 20.25 18V6A2.25 2.25 0 0 0 18 3.75H6A2.25 2.25 0 0 0 3.75 6v12A2.25 2.25 0 0 0 6 20.25Z"
/>
</svg>
Stats
</AppRailAnchor>
<svelte:fragment slot="trail"> <svelte:fragment slot="trail">
<div class="p-2"> <div class="p-2">

View File

@@ -1,21 +1,26 @@
<script lang="ts"> <script lang="ts">
import { HttpMethods, openApiStore } from '$lib'; import { HttpMethods } from '$lib';
import ParameterInput from '$lib/components/atoms/ParameterInput.svelte'; import ParameterInput from '$lib/components/atoms/ParameterInput.svelte';
import { getPathVariables } from '$lib/pathHandling'; import { getPathVariables } from '$lib/pathHandling';
import type { OpenAPIV3 } from '$lib/openAPITypes'; import type { OpenAPIV3_1 } from '$lib/openAPITypes';
import type { PageData } from './$types'; import type { PageData } from './$types';
import { Accordion, AccordionItem } from '@skeletonlabs/skeleton'; import { Accordion, AccordionItem, filter } from '@skeletonlabs/skeleton';
import { selectedSpec } from '$lib/db';
export let data: PageData; export let data: PageData;
const filterParams = (param: OpenAPIV3_1.ParameterObject | OpenAPIV3_1.ReferenceObject): param is OpenAPIV3_1.ParameterObject => {
return !("$ref" in param);
};
let newParam: 'query' | 'header' | 'cookie' = 'query'; let newParam: 'query' | 'header' | 'cookie' = 'query';
let tempPath: OpenAPIV3.PathItemObject = { let tempPath: OpenAPIV3_1.PathItemObject = {
parameters: [] parameters: []
}; };
openApiStore.subscribe((store) => { selectedSpec.subscribe((store) => {
if (!data.pathName) return; if (!data.pathName) return;
if (store.paths == undefined) tempPath = {}; if (store.spec.paths == undefined) tempPath = {};
if (!store.paths!.hasOwnProperty(data.pathName)) tempPath = {}; if (!store.spec.paths!.hasOwnProperty(data.pathName)) tempPath = {};
// @ts-expect-error - working with a known not empty object // @ts-expect-error - working with a known not empty object
tempPath = store.paths[data.pathName] ?? {}; tempPath = store.paths[data.pathName] ?? {};
@@ -96,9 +101,11 @@
<h4 class="h4">Parameters</h4> <h4 class="h4">Parameters</h4>
</svelte:fragment> </svelte:fragment>
<svelte:fragment slot="content"> <svelte:fragment slot="content">
{#each tempPath.parameters as param} {#if tempPath.parameters}
<ParameterInput variableName={param.name} bind:value={param} location="path" /> {#each tempPath.parameters.filter(filterParams) as param}
{/each} <ParameterInput variableName={param.name} bind:value={param} location="path" />
{/each}
{/if}
<span class="flex items-center gap-2"> <span class="flex items-center gap-2">
<select name="newParameter" bind:value={newParam} class="select w-min"> <select name="newParameter" bind:value={newParam} class="select w-min">
@@ -128,7 +135,8 @@
type="checkbox" type="checkbox"
class="checkbox" class="checkbox"
on:input={(event) => { on:input={(event) => {
if (event.target.checked) { //@ts-expect-error - working with a known object
if (event.target?.checked) {
tempPath[method] = { tempPath[method] = {
tags: [], tags: [],
summary: '', summary: '',

View File

@@ -1,14 +1,14 @@
import type { PageLoad } from './$types'; import type { PageLoad } from './$types';
import { openApiStore } from '$lib';
import { error } from '@sveltejs/kit'; import { error } from '@sveltejs/kit';
import type { OpenAPIV3_1 } from '$lib/openAPITypes'; import type { OpenAPIV3_1 } from '$lib/openAPITypes';
import { selectedSpec } from '$lib/db';
export const load = (async (event) => { export const load = (async (event) => {
// check if path param is an integer // check if path param is an integer
if (!/^\d+$/.test(event.params.index)) error(404, 'Invalid path index'); if (!/^\d+$/.test(event.params.index)) error(404, 'Invalid path index');
const index = parseInt(event.params.index); const index = parseInt(event.params.index);
let apiObject: OpenAPIV3_1.Document; let apiObject: OpenAPIV3_1.Document;
openApiStore.subscribe((value) => (apiObject = value)); selectedSpec.subscribe((value) => (apiObject = value.spec));
let pathName: string | undefined; let pathName: string | undefined;
// @ts-expect-error - svelte stores populate the value // @ts-expect-error - svelte stores populate the value
if (apiObject && !apiObject.paths) error(404, 'No paths found in the OpenAPI document'); if (apiObject && !apiObject.paths) error(404, 'No paths found in the OpenAPI document');

View File

@@ -1,27 +0,0 @@
<script lang="ts">
import { openApiStore, operationCount, pathCount } from '$lib';
const stats = [
{
title: 'Paths',
value: pathCount($openApiStore)
},
{
title: 'Operation IDs',
value: operationCount($openApiStore)
}
];
</script>
<div class="flex flex-row gap-4 justify-center">
{#each stats as stat}
<div
class="border-token rounded-container-token bg-surface-backdrop-token w-52 p-10 text-center flex flex-col gap-4"
>
<h2 class="text-lg">{stat.title}</h2>
<p class="text-lg">
{stat.value}
</p>
</div>
{/each}
</div>