mirror of
https://github.com/LukeHagar/OpenAPI.gg.git
synced 2025-12-06 04:20:29 +00:00
37
package.json
37
package.json
@@ -14,39 +14,40 @@
|
||||
"devDependencies": {
|
||||
"@skeletonlabs/skeleton": "2.10.0",
|
||||
"@skeletonlabs/tw-plugin": "0.4.0",
|
||||
"@sveltejs/adapter-auto": "^3.0.0",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||
"@sveltejs/adapter-auto": "^3.2.2",
|
||||
"@sveltejs/kit": "^2.5.18",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.1",
|
||||
"@tailwindcss/forms": "0.5.7",
|
||||
"@tailwindcss/typography": "0.5.13",
|
||||
"@types/eslint": "^8.56.0",
|
||||
"@types/eslint": "^8.56.10",
|
||||
"@types/node": "20.12.12",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||
"@typescript-eslint/parser": "^7.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.15.0",
|
||||
"@typescript-eslint/parser": "^7.15.0",
|
||||
"autoprefixer": "10.4.19",
|
||||
"eslint": "^8.56.0",
|
||||
"dexie": "^4.0.7",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.35.1",
|
||||
"eslint-plugin-svelte": "^2.41.0",
|
||||
"filenamify": "^6.0.0",
|
||||
"postcss": "8.4.38",
|
||||
"prettier": "^3.1.1",
|
||||
"prettier-plugin-svelte": "^3.1.2",
|
||||
"prettier": "^3.3.2",
|
||||
"prettier-plugin-svelte": "^3.2.5",
|
||||
"spdx-license-list": "^6.9.0",
|
||||
"svelte": "^4.2.7",
|
||||
"svelte-check": "^3.6.0",
|
||||
"svelte": "^4.2.18",
|
||||
"svelte-check": "^3.8.4",
|
||||
"tailwindcss": "3.4.3",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^5.0.3",
|
||||
"tslib": "^2.6.3",
|
||||
"typescript": "^5.5.3",
|
||||
"vite": "^5.3.3",
|
||||
"vite-plugin-tailwind-purgecss": "0.3.3",
|
||||
"yaml": "^2.4.2"
|
||||
"yaml": "^2.4.5"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "1.6.5",
|
||||
"@sveltejs/enhanced-img": "^0.2.0",
|
||||
"@sveltejs/enhanced-img": "^0.2.1",
|
||||
"openapi-types": "^12.1.3",
|
||||
"svelte-persisted-store": "^0.9.2"
|
||||
"svelte-persisted-store": "^0.9.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">20.0.0"
|
||||
|
||||
980
pnpm-lock.yaml
generated
980
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
18
src/lib/components/FileManagement/CreateNewButton.svelte
Normal file
18
src/lib/components/FileManagement/CreateNewButton.svelte
Normal file
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { newSpec, loadSpec } from '$lib/db';
|
||||
</script>
|
||||
|
||||
<button
|
||||
class="btn variant-ghost-success"
|
||||
on:click={() => {
|
||||
if (
|
||||
confirm(
|
||||
'This operation clears all the current values, unsaved data will be lost, are you sure?'
|
||||
)
|
||||
) {
|
||||
loadSpec(structuredClone(newSpec));
|
||||
}
|
||||
}}
|
||||
>
|
||||
Create New
|
||||
</button>
|
||||
19
src/lib/components/FileManagement/DeleteAllButton.svelte
Normal file
19
src/lib/components/FileManagement/DeleteAllButton.svelte
Normal file
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { newSpec } from '$lib/db';
|
||||
import { db, loadSpec, type APISpec } from '$lib/db';
|
||||
import type { CssClasses } from '@skeletonlabs/skeleton';
|
||||
|
||||
export let width: CssClasses = "w-full"
|
||||
</script>
|
||||
|
||||
<button
|
||||
class="btn variant-ghost-error {width}"
|
||||
on:click={async () => {
|
||||
if (confirm(`Are you sure you want to delete all saved specs?`)) {
|
||||
db.apiSpecs.clear();
|
||||
loadSpec(structuredClone(newSpec));
|
||||
}
|
||||
}}
|
||||
>
|
||||
Delete All
|
||||
</button>
|
||||
26
src/lib/components/FileManagement/DeleteButton.svelte
Normal file
26
src/lib/components/FileManagement/DeleteButton.svelte
Normal file
@@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { newSpec } from '$lib/db';
|
||||
import { db, loadSpec, type APISpec } from '$lib/db';
|
||||
import type { CssClasses } from '@skeletonlabs/skeleton';
|
||||
|
||||
export let spec: APISpec;
|
||||
|
||||
export let width: CssClasses = "w-full"
|
||||
</script>
|
||||
|
||||
<button
|
||||
class="btn variant-ghost-error {width}"
|
||||
on:click={async () => {
|
||||
if (confirm(`Are you sure you want to delete '${spec.name}'?`)) {
|
||||
await db.apiSpecs.delete(spec.id);
|
||||
const specs = await db.apiSpecs.toArray()
|
||||
if (specs.length > 0){
|
||||
loadSpec(specs[0]);
|
||||
} else {
|
||||
loadSpec(structuredClone(newSpec));
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
85
src/lib/components/FileManagement/DownloadButtons.svelte
Normal file
85
src/lib/components/FileManagement/DownloadButtons.svelte
Normal file
@@ -0,0 +1,85 @@
|
||||
<script lang="ts">
|
||||
import { selectedSpec } from '$lib/db';
|
||||
import filenamify from 'filenamify';
|
||||
import { stringify } from 'yaml';
|
||||
|
||||
$: fileName = filenamify($selectedSpec.spec?.info?.title) || 'openapi';
|
||||
|
||||
const saveYAML = () => {
|
||||
if (!$selectedSpec.spec) return;
|
||||
const openApi = $selectedSpec.spec;
|
||||
const blob = new Blob([stringify(openApi, null, { indent: 2, aliasDuplicateObjects: false })], {
|
||||
type: 'application/yaml'
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${fileName}.yaml`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const saveJSON = () => {
|
||||
if (!$selectedSpec.spec) return;
|
||||
const openApi = $selectedSpec.spec;
|
||||
const blob = new Blob([JSON.stringify(openApi, null, 2)], {
|
||||
type: 'application/json'
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${fileName}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="grid grid-cols-1 text-xs gap-2">
|
||||
<span class="text-center font-bold text-sm">Download</span>
|
||||
<button
|
||||
type="button"
|
||||
on:click={saveYAML}
|
||||
class="btn btn-sm rounded-lg w-full variant-ghost-tertiary gap-1"
|
||||
aria-label="Download a YAML representation of the OpenAPI document."
|
||||
>
|
||||
YAML
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="size-6"
|
||||
>
|
||||
<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.25m.75 12 3 3m0 0 3-3m-3 3v-6m-1.5-9H5.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>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
on:click={saveJSON}
|
||||
class="btn btn-sm rounded-lg w-full variant-ghost-tertiary gap-1"
|
||||
aria-label="Download a JSON representation of the OpenAPI document."
|
||||
>
|
||||
JSON
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="size-6"
|
||||
>
|
||||
<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.25m.75 12 3 3m0 0 3-3m-3 3v-6m-1.5-9H5.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>
|
||||
</button>
|
||||
</div>
|
||||
15
src/lib/components/FileManagement/LoadButton.svelte
Normal file
15
src/lib/components/FileManagement/LoadButton.svelte
Normal file
@@ -0,0 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { loadSpec, type APISpec } from '$lib/db';
|
||||
import type { CssClasses } from '@skeletonlabs/skeleton';
|
||||
|
||||
export let spec: APISpec;
|
||||
|
||||
export let width: CssClasses = "w-full"
|
||||
|
||||
function onLoad(e: Event): void {
|
||||
console.log('load button clicked');
|
||||
loadSpec(spec);
|
||||
}
|
||||
</script>
|
||||
|
||||
<button class="btn variant-ghost-warning {width}" on:click={onLoad}> Load </button>
|
||||
15
src/lib/components/FileManagement/SaveButton.svelte
Normal file
15
src/lib/components/FileManagement/SaveButton.svelte
Normal file
@@ -0,0 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { loadSpec, saveSpec, selectedSpec } from '$lib/db';
|
||||
import type { CssClasses } from '@skeletonlabs/skeleton';
|
||||
|
||||
export let width: CssClasses = "w-full"
|
||||
|
||||
async function onSave(e: Event): Promise<void> {
|
||||
console.log('Save button clicked');
|
||||
const spec = await saveSpec($selectedSpec);
|
||||
if(spec) loadSpec(spec);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<button class="btn variant-ghost-success {width}" on:click={onSave}> Save </button>
|
||||
12
src/lib/components/FileManagement/SaveNewButton.svelte
Normal file
12
src/lib/components/FileManagement/SaveNewButton.svelte
Normal file
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { db, selectedSpec } from '$lib/db';
|
||||
|
||||
function onSave(e: Event): void {
|
||||
console.log('Save button clicked');
|
||||
const newSpec = structuredClone($selectedSpec);
|
||||
newSpec.id = undefined;
|
||||
db.apiSpecs.put(newSpec);
|
||||
}
|
||||
</script>
|
||||
|
||||
<button class="btn variant-ghost-success" on:click={onSave}> Save As </button>
|
||||
16
src/lib/components/FileManagement/UploadButton.svelte
Normal file
16
src/lib/components/FileManagement/UploadButton.svelte
Normal 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>
|
||||
138
src/lib/components/FileManagement/UploadModal.svelte
Normal file
138
src/lib/components/FileManagement/UploadModal.svelte
Normal 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>
|
||||
@@ -5,7 +5,12 @@
|
||||
|
||||
export let schema: OpenAPIV3_1.SecuritySchemeObject;
|
||||
|
||||
let availableFlows: string[] = ['implicit', 'password', 'clientCredentials', 'authorizationCode'];
|
||||
let availableFlows: ('implicit' | 'password' | 'clientCredentials' | 'authorizationCode')[] = [
|
||||
'implicit',
|
||||
'password',
|
||||
'clientCredentials',
|
||||
'authorizationCode'
|
||||
];
|
||||
|
||||
// remove flows that are already in Object.keys(schema.flows)
|
||||
availableFlows = availableFlows.filter((flow) => {
|
||||
@@ -18,7 +23,7 @@
|
||||
return !Object.keys(schema.flows).includes(flow);
|
||||
});
|
||||
|
||||
let flowType: string;
|
||||
let flowType: 'implicit' | 'password' | 'clientCredentials' | 'authorizationCode';
|
||||
const addOauthFlow = () => {
|
||||
if (!flowType) return;
|
||||
// @ts-expect-error - security schema definition is lacking a bit
|
||||
@@ -27,7 +32,7 @@
|
||||
availableFlows = availableFlows.filter((flow) => flow !== flowType);
|
||||
};
|
||||
|
||||
const removeOauthFlow = (flow: string) => {
|
||||
const removeOauthFlow = (flow: 'implicit' | 'password' | 'clientCredentials' | 'authorizationCode') => {
|
||||
// @ts-expect-error - security schema definition is lacking a bit
|
||||
let tempFlows = schema.flows;
|
||||
delete tempFlows[flow];
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import type { OpenAPIV3 } from '$lib/openAPITypes';
|
||||
import type { OpenAPIV3_1 } from '$lib/openAPITypes';
|
||||
import { SlideToggle } from '@skeletonlabs/skeleton';
|
||||
|
||||
export let example: OpenAPIV3.ExampleObject;
|
||||
export let example: OpenAPIV3_1.ExampleObject | OpenAPIV3_1.ReferenceObject;
|
||||
export let name: string;
|
||||
|
||||
let schema = false;
|
||||
@@ -24,13 +24,17 @@
|
||||
<p>Description</p>
|
||||
<textarea class="textarea" name="description" bind:value={example.description} />
|
||||
</label>
|
||||
<label>
|
||||
<p>Value</p>
|
||||
<input type="text" class="input" name="value" bind:value={example.value} />
|
||||
</label>
|
||||
<label>
|
||||
<p>External Value</p>
|
||||
<input type="text" class="input" name="externalValue" bind:value={example.externalValue} />
|
||||
</label>
|
||||
{#if 'value' in example}
|
||||
<label>
|
||||
<p>Value</p>
|
||||
<input type="text" class="input" name="value" bind:value={example.value} />
|
||||
</label>
|
||||
{/if}
|
||||
{#if 'externalValue' in example}
|
||||
<label>
|
||||
<p>External Value</p>
|
||||
<input type="text" class="input" name="externalValue" bind:value={example.externalValue} />
|
||||
</label>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { openApiStore } from '$lib';
|
||||
import { selectedSpec } from '$lib/db';
|
||||
import spdxLicenseList from 'spdx-license-list';
|
||||
|
||||
const popularLicenses = ['MIT', 'Apache-2.0', 'GPL-3.0', 'Unlicense'];
|
||||
@@ -8,58 +8,80 @@
|
||||
<div class="border-token rounded-container-token space-y-1 p-4">
|
||||
<div class="flex flex-row justify-between">
|
||||
<h4 class="h4">License</h4>
|
||||
<label class="text-sm space-x-2">
|
||||
<span>Pick a license</span>
|
||||
<select
|
||||
class="select w-56 text-sm"
|
||||
bind:value={$openApiStore.info.license.identifier}
|
||||
on:change={() => {
|
||||
$openApiStore.info.license.name =
|
||||
spdxLicenseList[$openApiStore.info.license.identifier].name;
|
||||
}}
|
||||
>
|
||||
<optgroup label="Popular Licenses">
|
||||
{#each Object.keys(spdxLicenseList).filter( (entry) => popularLicenses.includes(entry) ) as license}
|
||||
<option value={license}>{spdxLicenseList[license].name}</option>
|
||||
{/each}
|
||||
</optgroup>
|
||||
<optgroup label="All Licenses">
|
||||
{#each Object.keys(spdxLicenseList).sort() as license}
|
||||
<option value={license}>{spdxLicenseList[license].name}</option>
|
||||
{/each}
|
||||
</optgroup>
|
||||
</select>
|
||||
</label>
|
||||
{#if $selectedSpec.spec.info.license}
|
||||
<label class="text-sm space-x-2">
|
||||
<span>Pick a license</span>
|
||||
<select
|
||||
class="select w-56 text-sm"
|
||||
bind:value={$selectedSpec.spec.info.license.identifier}
|
||||
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 =
|
||||
// @ts-expect-error - This is only running on identifier change
|
||||
spdxLicenseList[$selectedSpec.spec.info.license.identifier].name;
|
||||
}}
|
||||
>
|
||||
<optgroup label="Popular Licenses">
|
||||
{#each Object.keys(spdxLicenseList).filter( (entry) => popularLicenses.includes(entry) ) as license}
|
||||
<option value={license}>{spdxLicenseList[license].name}</option>
|
||||
{/each}
|
||||
</optgroup>
|
||||
<optgroup label="All Licenses">
|
||||
{#each Object.keys(spdxLicenseList).sort() as license}
|
||||
<option value={license}>{spdxLicenseList[license].name}</option>
|
||||
{/each}
|
||||
</optgroup>
|
||||
</select>
|
||||
</label>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<label class="text-sm space-y-1">
|
||||
<span>Name (required)</span>
|
||||
<input
|
||||
class="input text-sm"
|
||||
name="licenseName"
|
||||
placeholder="Apache 2.0"
|
||||
type="text"
|
||||
bind:value={$openApiStore.info.license.name}
|
||||
/>
|
||||
</label>
|
||||
<label class="text-sm space-y-1">
|
||||
<span>Identifer (optional)</span>
|
||||
<input
|
||||
class="input text-sm"
|
||||
name="licenseIdentifier"
|
||||
placeholder="Apache-2.0"
|
||||
type="text"
|
||||
bind:value={$openApiStore.info.license.identifier}
|
||||
/>
|
||||
</label>
|
||||
<label class="text-sm space-y-1">
|
||||
<span>URL (optional)</span>
|
||||
<input
|
||||
class="input text-sm"
|
||||
name="licenseUrl"
|
||||
placeholder="https://www.apache.org/licenses/LICENSE-2.0.html"
|
||||
type="url"
|
||||
bind:value={$openApiStore.info.license.url}
|
||||
/>
|
||||
</label>
|
||||
{#if $selectedSpec.spec.info.license}
|
||||
<label class="text-sm space-y-1">
|
||||
<span>Name (required)</span>
|
||||
<input
|
||||
class="input text-sm"
|
||||
name="licenseName"
|
||||
placeholder="Apache 2.0"
|
||||
type="text"
|
||||
bind:value={$selectedSpec.spec.info.license.name}
|
||||
/>
|
||||
</label>
|
||||
<label class="text-sm space-y-1">
|
||||
<span>Identifer (optional)</span>
|
||||
<input
|
||||
class="input text-sm"
|
||||
name="licenseIdentifier"
|
||||
placeholder="Apache-2.0"
|
||||
type="text"
|
||||
bind:value={$selectedSpec.spec.info.license.identifier}
|
||||
/>
|
||||
</label>
|
||||
<label class="text-sm space-y-1">
|
||||
<span>URL (optional)</span>
|
||||
<input
|
||||
class="input text-sm"
|
||||
name="licenseUrl"
|
||||
placeholder="https://www.apache.org/licenses/LICENSE-2.0.html"
|
||||
type="url"
|
||||
bind:value={$selectedSpec.spec.info.license.url}
|
||||
/>
|
||||
</label>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
class="btn variant-filled-primary"
|
||||
on:click={() => {
|
||||
$selectedSpec.spec.info.license = {
|
||||
name: '',
|
||||
identifier: '',
|
||||
url: ''
|
||||
};
|
||||
}}
|
||||
>
|
||||
Add License
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
bind:value={flow.authorizationUrl}
|
||||
/>
|
||||
</label>
|
||||
{#if type === 'authorizationCode'}
|
||||
{#if type === 'authorizationCode' && 'tokenUrl' in flow}
|
||||
<label>
|
||||
<h5 class="h5">Token URL</h5>
|
||||
<p class="text-sm">The token URL to be used for this flow.</p>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script lang="ts">
|
||||
import type { OpenAPIV3 } from '$lib/openAPITypes';
|
||||
import type { OpenAPIV3_1 } from '$lib/openAPITypes';
|
||||
import { SlideToggle } from '@skeletonlabs/skeleton';
|
||||
import ExampleInput from '$lib/components/atoms/ExampleInput.svelte';
|
||||
|
||||
export let variableName: string;
|
||||
export let value: OpenAPIV3.ParameterObject;
|
||||
export let value: OpenAPIV3_1.ParameterObject;
|
||||
export let location: 'path' | 'query' | 'header' | 'cookie';
|
||||
|
||||
value.name = variableName;
|
||||
@@ -104,9 +104,12 @@
|
||||
{#if multipleExamples}
|
||||
<div class="space-y-2">
|
||||
<p>Examples</p>
|
||||
{#each Object.keys(value.examples) as example}
|
||||
<ExampleInput bind:example={value.examples[example]} name={example} />
|
||||
{/each}
|
||||
{#if value.examples}
|
||||
{#each Object.entries(value.examples) as example}
|
||||
<ExampleInput bind:example={example[1]} name={example[0]} />
|
||||
{/each}
|
||||
|
||||
{/if}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm variant-filled-primary"
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { pathTemplate } from '$lib/pathTemplate';
|
||||
import {
|
||||
addPath,
|
||||
deletePath,
|
||||
openApiStore,
|
||||
pathRegex,
|
||||
renamePath,
|
||||
sortPathsAlphabetically
|
||||
renamePath
|
||||
} from '$lib';
|
||||
import { getModalStore } from '@skeletonlabs/skeleton';
|
||||
const modalStore = getModalStore();
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
<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 = () => {
|
||||
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>
|
||||
|
||||
<h4 class="h4">Scopes</h4>
|
||||
<table class="table">
|
||||
<tbody>
|
||||
{#each flow.scopes as scope, index}
|
||||
{#each Object.entries(flow.scopes) as scope, index}
|
||||
{#each scope as item, i}
|
||||
<tr>
|
||||
<td class="px-4">
|
||||
@@ -23,16 +28,7 @@
|
||||
name="scope{index}-{i}"
|
||||
class="input"
|
||||
placeholder="scope"
|
||||
bind:value={item.scope}
|
||||
/>
|
||||
</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}
|
||||
bind:value={item}
|
||||
/>
|
||||
</td>
|
||||
<td class="max-w-16 px-2">
|
||||
@@ -40,7 +36,7 @@
|
||||
type="button"
|
||||
class="btn variant-ringed-error hover:variant-filled-error"
|
||||
on:click={() => {
|
||||
removeScope(index);
|
||||
removeScope(item);
|
||||
}}
|
||||
>
|
||||
Remove Scope
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { openApiStore } from '$lib';
|
||||
import {
|
||||
apiKeyAuthTemplate,
|
||||
basicAuthTemplate,
|
||||
@@ -7,11 +6,12 @@
|
||||
oauth2AuthTemplate,
|
||||
openIdAuthTemplate
|
||||
} from '$lib/authTemplates';
|
||||
import { selectedSpec } from '$lib/db';
|
||||
import AuthenticationItem from '../atoms/AuthenticationItem.svelte';
|
||||
|
||||
let selectedSchema: string;
|
||||
const addSecuritySchema = () => {
|
||||
let tempSchemaList = $openApiStore.security;
|
||||
let tempSchemaList = $selectedSpec.spec.security || [];
|
||||
let newSchema;
|
||||
switch (selectedSchema) {
|
||||
case 'basicAuth':
|
||||
@@ -36,47 +36,61 @@
|
||||
|
||||
if (newSchema) {
|
||||
tempSchemaList = [...tempSchemaList, newSchema];
|
||||
$openApiStore.security = tempSchemaList;
|
||||
$selectedSpec.spec.security = tempSchemaList;
|
||||
}
|
||||
};
|
||||
const removeSecuritySchema = (index: number) => {
|
||||
let tempSchemaList = $openApiStore.security;
|
||||
let tempSchemaList = $selectedSpec.spec.security;
|
||||
tempSchemaList.splice(index, 1);
|
||||
$openApiStore.security = tempSchemaList;
|
||||
$selectedSpec.spec.security = tempSchemaList;
|
||||
};
|
||||
</script>
|
||||
|
||||
<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">
|
||||
<div class="flex flex-row-reverse w-full">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm variant-ringed-error hover:variant-filled-error"
|
||||
on:click={() => {
|
||||
removeSecuritySchema(index);
|
||||
}}
|
||||
>
|
||||
Remove schema
|
||||
</button>
|
||||
{#if $selectedSpec.spec.security}
|
||||
<form
|
||||
class="border-token rounded-container-token bg-surface-backdrop-token px-6 py-4 min-h-20 space-y-4"
|
||||
>
|
||||
{#each $selectedSpec.spec.security as schema, index}
|
||||
<div class="card w-full p-4">
|
||||
<div class="flex flex-row-reverse w-full">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm variant-ringed-error hover:variant-filled-error"
|
||||
on:click={() => {
|
||||
removeSecuritySchema(index);
|
||||
}}
|
||||
>
|
||||
Remove schema
|
||||
</button>
|
||||
</div>
|
||||
<AuthenticationItem bind:schema />
|
||||
</div>
|
||||
<AuthenticationItem bind:schema />
|
||||
</div>
|
||||
<hr />
|
||||
{/each}
|
||||
<hr />
|
||||
{/each}
|
||||
|
||||
<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">
|
||||
<option value="basicAuth" selected>Basic Auth</option>
|
||||
<option value="bearerAuth">Bearer Auth</option>
|
||||
<option value="ApiKeyAuth">API Key Auth</option>
|
||||
<option value="openId">OpenID</option>
|
||||
<option value="oAuthSample">OAuth2</option>
|
||||
</select>
|
||||
<button type="button" class="btn text-sm variant-filled-primary" on:click={addSecuritySchema}>
|
||||
Add Security Schema
|
||||
<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">
|
||||
<option value="basicAuth" selected>Basic Auth</option>
|
||||
<option value="bearerAuth">Bearer Auth</option>
|
||||
<option value="ApiKeyAuth">API Key Auth</option>
|
||||
<option value="openId">OpenID</option>
|
||||
<option value="oAuthSample">OAuth2</option>
|
||||
</select>
|
||||
<button type="button" class="btn text-sm variant-filled-primary" on:click={addSecuritySchema}>
|
||||
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>
|
||||
</span>
|
||||
</form>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import Info from '../icons/Info.svelte';
|
||||
import LicenseAtom from '../atoms/LicenseAtom.svelte';
|
||||
import { openApiStore } from '$lib';
|
||||
import { selectedSpec } from '$lib/db';
|
||||
</script>
|
||||
|
||||
<form class="space-y-2">
|
||||
@@ -13,7 +13,7 @@
|
||||
name="title"
|
||||
placeholder="Sample API"
|
||||
type="text"
|
||||
bind:value={$openApiStore.info.title}
|
||||
bind:value={$selectedSpec.spec.info.title}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
@@ -23,7 +23,7 @@
|
||||
class="textarea"
|
||||
name="description"
|
||||
placeholder="Optional multiline or single-line description. Supports Markdown."
|
||||
bind:value={$openApiStore.info.description}
|
||||
bind:value={$selectedSpec.spec.info.description}
|
||||
/>
|
||||
</label>
|
||||
<label class="space-y-1">
|
||||
@@ -33,7 +33,7 @@
|
||||
name="version"
|
||||
placeholder="0.1.0"
|
||||
type="text"
|
||||
bind:value={$openApiStore.info.version}
|
||||
bind:value={$selectedSpec.spec.info.version}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
@@ -44,42 +44,59 @@
|
||||
name="termsOfService"
|
||||
placeholder="https://example.com/terms"
|
||||
type="url"
|
||||
bind:value={$openApiStore.info.termsOfService}
|
||||
bind:value={$selectedSpec.spec.info.termsOfService}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="border-token rounded-container-token bg-surface-backdrop-token space-y-1 p-4">
|
||||
<h4 class="h4">Contact Information</h4>
|
||||
<label class="space-y-1">
|
||||
<span class="text-sm">Name (optional)</span>
|
||||
<input
|
||||
class="input text-sm"
|
||||
name="contactName"
|
||||
placeholder="John Doe"
|
||||
type="text"
|
||||
bind:value={$openApiStore.info.contact.name}
|
||||
/>
|
||||
</label>
|
||||
<label class="space-y-1">
|
||||
<span class="text-sm">Email (optional)</span>
|
||||
<input
|
||||
class="input text-sm"
|
||||
name="contactEmail"
|
||||
placeholder="email@example.com"
|
||||
type="email"
|
||||
bind:value={$openApiStore.info.contact.email}
|
||||
/>
|
||||
</label>
|
||||
<label class="space-y-1">
|
||||
<span class="text-sm">URL (optional)</span>
|
||||
<input
|
||||
class="input text-sm"
|
||||
name="contactUrl"
|
||||
placeholder="https://example.com"
|
||||
type="url"
|
||||
bind:value={$openApiStore.info.contact.url}
|
||||
/>
|
||||
</label>
|
||||
{#if $selectedSpec.spec.info.contact}
|
||||
<label class="space-y-1">
|
||||
<span class="text-sm">Name (optional)</span>
|
||||
<input
|
||||
class="input text-sm"
|
||||
name="contactName"
|
||||
placeholder="John Doe"
|
||||
type="text"
|
||||
bind:value={$selectedSpec.spec.info.contact.name}
|
||||
/>
|
||||
</label>
|
||||
<label class="space-y-1">
|
||||
<span class="text-sm">Email (optional)</span>
|
||||
<input
|
||||
class="input text-sm"
|
||||
name="contactEmail"
|
||||
placeholder="email@example.com"
|
||||
type="email"
|
||||
bind:value={$selectedSpec.spec.info.contact.email}
|
||||
/>
|
||||
</label>
|
||||
<label class="space-y-1">
|
||||
<span class="text-sm">URL (optional)</span>
|
||||
<input
|
||||
class="input text-sm"
|
||||
name="contactUrl"
|
||||
placeholder="https://example.com"
|
||||
type="url"
|
||||
bind:value={$selectedSpec.spec.info.contact.url}
|
||||
/>
|
||||
</label>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
class="btn variant-filled-primary"
|
||||
on:click={() => {
|
||||
$selectedSpec.spec.info.contact = {
|
||||
name: '',
|
||||
email: '',
|
||||
url: ''
|
||||
};
|
||||
}}
|
||||
>
|
||||
Add Contact
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<LicenseAtom />
|
||||
</form>
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { addPath } from '$lib';
|
||||
import { openApiStore, sortPathsAlphabetically } from '$lib';
|
||||
import type { OpenAPIV3 } from '$lib/openAPITypes';
|
||||
import { sortPathsAlphabetically } from '$lib';
|
||||
import { selectedSpec } from '$lib/db';
|
||||
import type { OpenAPIV3_1 } from '$lib/openAPITypes';
|
||||
import PathListItem from '../atoms/PathListItem.svelte';
|
||||
import { getModalStore } from '@skeletonlabs/skeleton';
|
||||
|
||||
let paths: OpenAPIV3.PathsObject = {};
|
||||
// @ts-expect-error - working with a potentially empty object
|
||||
openApiStore.subscribe((store) => (paths = store.paths));
|
||||
let paths: OpenAPIV3_1.PathsObject = {};
|
||||
selectedSpec.subscribe((store) => {
|
||||
if (store.spec.paths) {
|
||||
paths = store.spec.paths;
|
||||
}
|
||||
});
|
||||
|
||||
const modalStore = getModalStore();
|
||||
</script>
|
||||
@@ -69,6 +73,38 @@
|
||||
<div
|
||||
class="mx-auto border-token rounded-container-token bg-surface-backdrop-token px-6 py-4 space-y-4"
|
||||
>
|
||||
{#if Object.keys(paths).length === 0}
|
||||
<p class="text-center">Wow such empty.</p>
|
||||
<p class="text-center">Please consider adding a path to your API.</p>
|
||||
<!-- This is very important. Do not remove :) -->
|
||||
<pre class="flex justify-center text-xs">
|
||||
⠀⠀⠀⡟⠋⠈⠙⣦⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⠤⢤⡀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠈⢇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠞⠀⠀⢠⡜⣦⠀
|
||||
*wow*⠀⠀⠀⠀⠀⠀⠀⡃⠀⠀⠀⠀⠈⢷⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠊⣠⠀⠀⠀⠀⢻⡘⡇
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠃⠀⠀⠀⠀⠀⠀⠙⢶⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡠⠚⢀⡼⠃⠀⠀⠀⠀⠸⣇⢳
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣾⠀⣀⠖⠀⠀⠀⠀⠉⠀⠀⠈⠉⠛⠛⡛⢛⠛⢳⡶⠖⠋⠀⢠⡞⠀⠀⠀⠐⠆⠀⠀⣿⢸
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠻⣦⣀⣴⡟⠀⠀⢶⣶⣾⡿⠀⠀⣿⢸ *such empty*
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⠞⠁⠀⠀⠀⠀⠀⠀⠀⠀⡠⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠻⣏⠀⠀⠀⣶⣿⣿⡇⠀⠀⢏⡞
|
||||
⠀⠀⠀⠀⠀⠀⢀⡴⠛⠀⠀⠀⠀⠀⠀⠀⠀⢀⢀⡾⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⢦⣤⣾⣿⣿⠋⠀⠀⡀⣾⠁
|
||||
⠀⠀⠀⠀⠀⣠⠟⠁⠀⠀⠀⣀⠀⠀⠀⠀⢀⡟⠈⢀⣤⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠙⣏⡁⠀⠐⠚⠃⣿⠀
|
||||
⠀⠀⠀⠀⣴⠋⠀⠀⠀⡴⣿⣿⡟⣷⠀⠀⠊⠀⠴⠛⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠀⠀⠀⠀⢹⡆
|
||||
⠀⠀⠀⣴⠃⠀⠀⠀⠀⣇⣿⣿⣿⠃⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⡶⢶⣶⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇
|
||||
*very sadge*⠀⠀⣸⠃⠀⠀⠀⢠⠀⠊⠛⠉⠁⠀⠀⠀⠀⠀⠀⠀⢲⣾⣿⡏⣾⣿⣿⣿⣿⠖⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢧
|
||||
⠀⢠⡇⠀⠀⠀⠀⠈⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠈⠛⠿⣽⣿⡿⠏⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡜
|
||||
⢀⡿⠀⠀⠀⠀⢀⣤⣶⣟⣶⣦⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇
|
||||
⢸⠇⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇
|
||||
⣼⠀⢀⡀⠀⠀⢷⣿⣿⣿⣿⣿⣿⡿⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⡇
|
||||
⡇⠀⠈⠀⠀⠀⣬⠻⣿⣿⣿⡿⠙⠀⠀⢀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⠁
|
||||
⢹⡀⠀⠀⠀⠈⣿⣶⣿⣿⣝⡛⢳⠭⠍⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⠃⠀
|
||||
⠸⡇⠀⠀⠀⠀⠙⣿⣿⣿⣿⣿⣿⣷⣦⣀⣀⣀⣤⣤⣴⡶⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⠇⠀⠀
|
||||
⠀⢿⡄⠀⠀⠀⠀⠀⠙⣇⠉⠉⠙⠛⠻⠟⠛⠛⠉⠙⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡰⠋⠀⠀⠀
|
||||
⠀⠈⢧⠀⠀⠀⠀⠀⠀⠈⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⠞⠁⠀⠀⠀⠀
|
||||
⠀⠀⠘⢷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⠞⠁⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠱⢆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⡴⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠛⢦⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣠⠴⠟⠁ *many space*
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⠈⠛⠲⠤⣤⣤⣤⣄⠀⠀⠀⠀⠀⠀⠀⢠⣤⣤⠤⠴⠒⠛⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
</pre>
|
||||
{/if}
|
||||
{#each Object.keys(paths) as pathName, index}
|
||||
<PathListItem {pathName} id={index} />
|
||||
{/each}
|
||||
|
||||
@@ -1,35 +1,35 @@
|
||||
<script lang="ts">
|
||||
import { openApiStore } from '$lib';
|
||||
import { selectedSpec } from '$lib/db';
|
||||
import ServerInput from '../atoms/ServerInput.svelte';
|
||||
|
||||
const addServer = () => {
|
||||
// check if the servers array is not undefined
|
||||
if (!$openApiStore.servers) {
|
||||
$openApiStore.servers = [];
|
||||
if (!$selectedSpec.spec.servers) {
|
||||
$selectedSpec.spec.servers = [];
|
||||
}
|
||||
let tempServers = [...$openApiStore.servers];
|
||||
let tempServers = [...$selectedSpec.spec.servers];
|
||||
tempServers.push({ url: '', description: '' });
|
||||
$openApiStore.servers = tempServers;
|
||||
$selectedSpec.spec.servers = tempServers;
|
||||
};
|
||||
|
||||
const removeServer = (index: number) => {
|
||||
// check if the servers array is not undefined
|
||||
if (!$openApiStore.servers) {
|
||||
$openApiStore.servers = [];
|
||||
if (!$selectedSpec.spec.servers) {
|
||||
$selectedSpec.spec.servers = [];
|
||||
}
|
||||
let tempServers = [...$openApiStore.servers];
|
||||
let tempServers = [...$selectedSpec.spec.servers];
|
||||
tempServers.splice(index, 1);
|
||||
$openApiStore.servers = tempServers;
|
||||
$selectedSpec.spec.servers = tempServers;
|
||||
};
|
||||
</script>
|
||||
|
||||
<form
|
||||
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 Array.isArray($openApiStore.servers)}
|
||||
<!-- If selectedSpec$selectedSpec.spec.servers isnt an array show add button -->
|
||||
{#if Array.isArray($selectedSpec.spec.servers)}
|
||||
<ul class="space-y-6">
|
||||
{#each $openApiStore.servers as server, index}
|
||||
{#each $selectedSpec.spec.servers as server, index}
|
||||
<li class="!block">
|
||||
<span class="flex w-full justify-between">
|
||||
<span>Server {index + 1} </span>
|
||||
@@ -43,12 +43,12 @@
|
||||
</span>
|
||||
<ServerInput id={1} bind:server />
|
||||
</li>
|
||||
{#if index < $openApiStore.servers.length - 1}
|
||||
{#if index < $selectedSpec.spec.servers.length - 1}
|
||||
<hr />
|
||||
{/if}
|
||||
{/each}
|
||||
</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}>
|
||||
Add Server
|
||||
</button>
|
||||
|
||||
94
src/lib/db.ts
Normal file
94
src/lib/db.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import type { OpenAPIV3_1 } from "$lib/openAPITypes";
|
||||
import Dexie, { type Table } from 'dexie';
|
||||
import { persisted } from "svelte-persisted-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 = {
|
||||
name: 'OpenAPI',
|
||||
spec: blankSpec
|
||||
} as const
|
||||
|
||||
export const selectedSpecId: Writable<string | undefined> = persisted("selectedSpecId",undefined)
|
||||
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 {
|
||||
id?: string;
|
||||
name: string;
|
||||
spec: OpenAPIV3_1.Document;
|
||||
}
|
||||
|
||||
export class MySubClassedDexie extends Dexie {
|
||||
apiSpecs!: Table<APISpec>;
|
||||
|
||||
constructor() {
|
||||
super('oasDesigner');
|
||||
this.version(1).stores({
|
||||
apiSpecs: '++id, name, spec', // Primary key and indexed props
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { persisted } from 'svelte-persisted-store';
|
||||
import type { OpenAPIV3_1 } from './openAPITypes';
|
||||
import { writable, type Writable } from 'svelte/store';
|
||||
|
||||
export const localStoragePrefix = 'openapigen-';
|
||||
|
||||
@@ -12,51 +12,16 @@ export const operationCount = (openApiDoc: OpenAPIV3_1.Document) => {
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
};
|
||||
|
||||
export const pathCount = (openApiDoc: OpenAPIV3_1.Document) => {
|
||||
let count = 0;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
for (const path in openApiDoc.paths) {
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
export const openApiStore = persisted<OpenAPIV3_1.Document>(`${localStoragePrefix}openApi`, {
|
||||
openapi: '3.1.0', // OpenAPI 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: {},
|
||||
components: {},
|
||||
security: [],
|
||||
tags: [],
|
||||
externalDocs: {
|
||||
description: '',
|
||||
url: ''
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export enum HttpMethods {
|
||||
GET = 'get',
|
||||
|
||||
81
src/lib/openAPITypes.d.ts
vendored
81
src/lib/openAPITypes.d.ts
vendored
@@ -15,13 +15,14 @@ export declare namespace OpenAPIV3_1 {
|
||||
info: InfoObject;
|
||||
jsonSchemaDialect?: string;
|
||||
servers?: ServerObject[];
|
||||
security?: SecurityRequirementObject[];
|
||||
} & (
|
||||
| (Pick<PathsWebhooksComponents<T>, 'paths'> &
|
||||
Omit<Partial<PathsWebhooksComponents<T>>, 'paths'>)
|
||||
Omit<Partial<PathsWebhooksComponents<T>>, 'paths'>)
|
||||
| (Pick<PathsWebhooksComponents<T>, 'webhooks'> &
|
||||
Omit<Partial<PathsWebhooksComponents<T>>, 'webhooks'>)
|
||||
Omit<Partial<PathsWebhooksComponents<T>>, 'webhooks'>)
|
||||
| (Pick<PathsWebhooksComponents<T>, 'components'> &
|
||||
Omit<Partial<PathsWebhooksComponents<T>>, 'components'>)
|
||||
Omit<Partial<PathsWebhooksComponents<T>>, 'components'>)
|
||||
)
|
||||
>;
|
||||
export type InfoObject = Modify<
|
||||
@@ -64,8 +65,8 @@ export declare namespace OpenAPIV3_1 {
|
||||
parameters?: (ReferenceObject | ParameterObject)[];
|
||||
}
|
||||
> & {
|
||||
[method in HttpMethods]?: OperationObject<T>;
|
||||
};
|
||||
[method in HttpMethods]?: OperationObject<T>;
|
||||
};
|
||||
export type OperationObject<T extends {} = {}> = Modify<
|
||||
OpenAPIV3.OperationObject<T>,
|
||||
{
|
||||
@@ -192,7 +193,9 @@ export declare namespace OpenAPIV3_1 {
|
||||
export type OAuth2SecurityScheme = OpenAPIV3.OAuth2SecurityScheme;
|
||||
export type OpenIdSecurityScheme = OpenAPIV3.OpenIdSecurityScheme;
|
||||
export type TagObject = OpenAPIV3.TagObject;
|
||||
export {};
|
||||
export type OAuth2Flows = OpenAPIV3.OAuth2Flows;
|
||||
export type OAuth2Scopes = OpenAPIV3.OAuth2Scopes;
|
||||
export { };
|
||||
}
|
||||
|
||||
export declare namespace OpenAPIV3 {
|
||||
@@ -267,8 +270,8 @@ export declare namespace OpenAPIV3 {
|
||||
servers?: ServerObject[];
|
||||
parameters?: (ReferenceObject | ParameterObject)[];
|
||||
} & {
|
||||
[method in HttpMethods]?: OperationObject<T>;
|
||||
};
|
||||
[method in HttpMethods]?: OperationObject<T>;
|
||||
};
|
||||
type OperationObject<T extends {} = {}> = {
|
||||
tags?: string[];
|
||||
summary?: string;
|
||||
@@ -293,7 +296,7 @@ export declare namespace OpenAPIV3 {
|
||||
name: string;
|
||||
in: string;
|
||||
}
|
||||
interface HeaderObject extends ParameterBaseObject {}
|
||||
interface HeaderObject extends ParameterBaseObject { }
|
||||
interface ParameterBaseObject {
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
@@ -483,40 +486,38 @@ export declare namespace OpenAPIV3 {
|
||||
name: 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 {
|
||||
type: 'oauth2';
|
||||
description?: string;
|
||||
flows: {
|
||||
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;
|
||||
};
|
||||
};
|
||||
};
|
||||
flows: OAuth2Flows;
|
||||
}
|
||||
interface OpenIdSecurityScheme {
|
||||
type: 'openIdConnect';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { get } from 'svelte/store';
|
||||
import type { ModalSettings, ModalStore } from '@skeletonlabs/skeleton';
|
||||
import { pathTemplate } from './pathTemplate';
|
||||
import { openApiStore } from '$lib';
|
||||
import type { OpenAPIV3 } from './openAPITypes';
|
||||
import type { OpenAPIV3_1 } from './openAPITypes';
|
||||
import { selectedSpec } from './db';
|
||||
|
||||
export const pathVariables = /\{([^}]+)\}/gm;
|
||||
|
||||
@@ -39,9 +39,9 @@ export const addPath = (modalStore: ModalStore, startingPoint: string = '/') =>
|
||||
}
|
||||
|
||||
// create path object
|
||||
const store = get(openApiStore);
|
||||
if (!store.paths) store.paths = {};
|
||||
store.paths[userPath] = pathTemplate;
|
||||
const store = get(selectedSpec);
|
||||
if (!store.spec.paths) store.spec.paths = {};
|
||||
store.spec.paths[userPath] = pathTemplate;
|
||||
|
||||
// sort paths alphabetically
|
||||
sortPathsAlphabetically();
|
||||
@@ -80,16 +80,16 @@ export const renamePath = (modalStore: ModalStore, oldPath: string) => {
|
||||
}
|
||||
|
||||
// create path object
|
||||
const store = get(openApiStore);
|
||||
if (!store.paths) store.paths = {};
|
||||
const store = get(selectedSpec);
|
||||
if (!store.spec.paths) store.spec.paths = {};
|
||||
|
||||
// copy old path object to new path object
|
||||
store.paths[userPath] = store.paths[oldPath];
|
||||
store.spec.paths[userPath] = store.spec.paths[oldPath];
|
||||
// delete old path object
|
||||
delete store.paths[oldPath];
|
||||
delete store.spec.paths[oldPath];
|
||||
|
||||
// add path to store
|
||||
openApiStore.set(store);
|
||||
selectedSpec.set(store);
|
||||
|
||||
// sort paths alphabetically
|
||||
sortPathsAlphabetically();
|
||||
@@ -109,12 +109,12 @@ export const deletePath = (modalStore: ModalStore, path: string) => {
|
||||
// TRUE if confirm pressed, FALSE if cancel pressed
|
||||
response: (r: boolean) => {
|
||||
if (r === false) return;
|
||||
const store = get(openApiStore);
|
||||
const store = get(selectedSpec);
|
||||
// check if path exists
|
||||
if (!store.paths) return;
|
||||
if (!(path in store.paths)) return;
|
||||
delete store.paths[path];
|
||||
openApiStore.set(store);
|
||||
if (!store.spec.paths) return;
|
||||
if (!(path in store.spec.paths)) return;
|
||||
delete store.spec.paths[path];
|
||||
selectedSpec.set(store);
|
||||
}
|
||||
};
|
||||
modalStore.trigger(modal);
|
||||
@@ -122,8 +122,8 @@ export const deletePath = (modalStore: ModalStore, path: string) => {
|
||||
|
||||
/// checks if a given path already exists
|
||||
export const pathExists = (path: string) => {
|
||||
const store = get(openApiStore);
|
||||
return !(path in store.paths!);
|
||||
const store = get(selectedSpec);
|
||||
return !(path in store.spec.paths!);
|
||||
};
|
||||
|
||||
/// checks if a given path is valid
|
||||
@@ -139,7 +139,7 @@ export const isValidPath = (path: string) => {
|
||||
// check if path is a valid path
|
||||
const pathWithoutVariables = path.replaceAll('{', '').replaceAll('}', '');
|
||||
console.log(pathWithoutVariables);
|
||||
const pathRegex = /(\/[a-zA-Z-]+)+|\//gm;
|
||||
const pathRegex = /(\/[a-zA-Z0-9]+)+|\//gm;
|
||||
const pathParts = pathWithoutVariables.match(pathRegex);
|
||||
// the fallback is to return false if the pathParts array is null
|
||||
return pathParts?.length === 1;
|
||||
@@ -148,18 +148,18 @@ export const isValidPath = (path: string) => {
|
||||
/// sorts the paths in the OpenAPI document alphabetically
|
||||
export const sortPathsAlphabetically = () => {
|
||||
const tempPathObject = {};
|
||||
const store = get(openApiStore);
|
||||
const store = get(selectedSpec);
|
||||
// @ts-expect-error - we are working with an initially empty object
|
||||
Object.keys(store.paths)
|
||||
Object.keys(store.spec.paths)
|
||||
.sort()
|
||||
.forEach((key) => {
|
||||
// @ts-expect-error - we are working with initially empty objects
|
||||
tempPathObject[key] = store.paths[key];
|
||||
tempPathObject[key] = store.spec.paths[key];
|
||||
});
|
||||
|
||||
// update path object
|
||||
openApiStore.update((data) => {
|
||||
data.paths = tempPathObject;
|
||||
selectedSpec.update((data) => {
|
||||
data.spec.paths = tempPathObject;
|
||||
return data;
|
||||
});
|
||||
};
|
||||
@@ -171,7 +171,7 @@ export const getPathVariables = (path: string) => {
|
||||
return variables.map((variable) => variable.replace('{', '').replace('}', ''));
|
||||
};
|
||||
|
||||
export const sortPathParameters = (parameters: OpenAPIV3.ParameterObject[]) => {
|
||||
export const sortPathParameters = (parameters: OpenAPIV3_1.ParameterObject[]) => {
|
||||
const tempParameters = parameters;
|
||||
tempParameters.sort((a, b) => {
|
||||
if (a.in < b.in) return -1;
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
<script lang="ts">
|
||||
import FancyAppRail from './FancyAppRail.svelte';
|
||||
|
||||
import { AppShell, type ModalComponent } from '@skeletonlabs/skeleton';
|
||||
import '../app.postcss';
|
||||
import { AppBar, AppShell } from '@skeletonlabs/skeleton';
|
||||
|
||||
// Floating UI for Popups
|
||||
import { computePosition, autoUpdate, flip, shift, offset, arrow } from '@floating-ui/dom';
|
||||
// Floating UI for Popups
|
||||
import { arrow, autoUpdate, computePosition, flip, offset, shift } from '@floating-ui/dom';
|
||||
import { storePopup } from '@skeletonlabs/skeleton';
|
||||
import { localStoragePrefix } from '$lib';
|
||||
storePopup.set({ computePosition, autoUpdate, flip, shift, offset, arrow });
|
||||
|
||||
// Modal
|
||||
import { initializeStores, Modal, LightSwitch } from '@skeletonlabs/skeleton';
|
||||
// Modal
|
||||
import { initializeStores, Modal } from '@skeletonlabs/skeleton';
|
||||
import UploadModal from '$lib/components/FileManagement/UploadModal.svelte';
|
||||
initializeStores();
|
||||
|
||||
const components: Record<string, ModalComponent> = {
|
||||
// Set a unique modal ID, then pass the component reference
|
||||
uploadModal: { ref: UploadModal },
|
||||
// ...
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<Modal />
|
||||
<Modal {components} />
|
||||
|
||||
<AppShell slotPageContent="px-6 py-4">
|
||||
<svelte:fragment slot="sidebarLeft">
|
||||
|
||||
@@ -1,23 +1,120 @@
|
||||
<div class="w-full h-full flex flex-col items-center justify-center grow">
|
||||
<h1 class="h1">
|
||||
<span
|
||||
class="bg-gradient-to-br from-blue-500 to-cyan-300 bg-clip-text text-transparent box-decoration-clone"
|
||||
>
|
||||
Design.
|
||||
</span>
|
||||
</h1>
|
||||
<h1 class="h1">
|
||||
<span
|
||||
class="bg-gradient-to-br from-red-500 to-yellow-500 bg-clip-text text-transparent box-decoration-clone"
|
||||
>
|
||||
Build.
|
||||
</span>
|
||||
</h1>
|
||||
<h1 class="h1">
|
||||
<span
|
||||
class="bg-gradient-to-br from-pink-500 to-violet-500 bg-clip-text text-transparent box-decoration-clone"
|
||||
>
|
||||
Deploy.
|
||||
</span>
|
||||
</h1>
|
||||
<script lang="ts">
|
||||
import { pathCount, operationCount } from '$lib';
|
||||
import CreateNewButton from '$lib/components/FileManagement/CreateNewButton.svelte';
|
||||
import DeleteAllButton from '$lib/components/FileManagement/DeleteAllButton.svelte';
|
||||
import DeleteButton from '$lib/components/FileManagement/DeleteButton.svelte';
|
||||
import LoadButton from '$lib/components/FileManagement/LoadButton.svelte';
|
||||
import SaveButton from '$lib/components/FileManagement/SaveButton.svelte';
|
||||
import SaveNewButton from '$lib/components/FileManagement/SaveNewButton.svelte';
|
||||
import UploadButton from '$lib/components/FileManagement/UploadButton.svelte';
|
||||
import { db, loadSpec, newSpec, selectedSpec, selectedSpecId } from '$lib/db';
|
||||
import { liveQuery } from 'dexie';
|
||||
import { ProgressRadial } from '@skeletonlabs/skeleton';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let apiSpecs = liveQuery(() => db.apiSpecs.toArray());
|
||||
let specLoaded = false;
|
||||
let pageLoaded = false;
|
||||
|
||||
apiSpecs.subscribe((specs) => {
|
||||
if (specLoaded) return;
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
$: 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>
|
||||
|
||||
<div class="grid place-content-center h-full gap-2 px-1">
|
||||
{#if !specLoaded && !pageLoaded}
|
||||
<ProgressRadial />
|
||||
{:else}
|
||||
<div class="card flex flex-col gap-4 p-4">
|
||||
<p class="text-lg font-bold">Selected Spec</p>
|
||||
<label class="text-lg">
|
||||
<span class="font-semibold">Name:</span>
|
||||
<input type="text" bind:value={$selectedSpec.name} class="input w-full" />
|
||||
</label>
|
||||
<div>
|
||||
<p><span class="font-semibold text-lg">Id:</span> {$selectedSpec.id || 'Not Saved'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold text-lg">Stats:</p>
|
||||
{#each stats as stat}
|
||||
<p class="">
|
||||
{stat.title}: {stat.value}
|
||||
</p>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="flex flex-row justify-center gap-2">
|
||||
{#if $selectedSpec.id}
|
||||
<CreateNewButton />
|
||||
{/if}
|
||||
<SaveButton />
|
||||
{#if $selectedSpec.id}
|
||||
<SaveNewButton />
|
||||
<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}
|
||||
</div>
|
||||
|
||||
@@ -1,45 +1,15 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
AppRail,
|
||||
AppRailAnchor,
|
||||
AppRailTile,
|
||||
FileButton,
|
||||
LightSwitch
|
||||
} from '@skeletonlabs/skeleton';
|
||||
import { page } from '$app/stores';
|
||||
import { localStoragePrefix } from '$lib';
|
||||
import { goto } from '$app/navigation';
|
||||
import { parse, stringify } from 'yaml';
|
||||
import { openApiStore } from '$lib';
|
||||
import filenamify from 'filenamify';
|
||||
|
||||
let files: FileList | undefined;
|
||||
|
||||
$: fileName = filenamify($openApiStore.info.title) || 'openapi';
|
||||
|
||||
function onFileUpload(e: Event): void {
|
||||
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');
|
||||
try {
|
||||
if (isJson) {
|
||||
openApiStore.set(JSON.parse(result));
|
||||
} else {
|
||||
openApiStore.set(parse(result));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error parsing ${isJson ? 'json' : 'yaml'} file`, error);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
import DownloadButtons from '$lib/components/FileManagement/DownloadButtons.svelte';
|
||||
import { AppRail, AppRailAnchor, LightSwitch } from '@skeletonlabs/skeleton';
|
||||
</script>
|
||||
|
||||
<AppRail width="w-28" aspectRatio="aspect-[3/2]" background="variant-ghost-surface" border="ring-0">
|
||||
<AppRail
|
||||
width="w-28"
|
||||
aspectRatio="aspect-[20/14]"
|
||||
background="variant-ghost-surface"
|
||||
border="ring-0"
|
||||
>
|
||||
<svelte:fragment slot="lead">
|
||||
<div>
|
||||
<AppRailAnchor href="/">
|
||||
@@ -105,7 +75,7 @@
|
||||
d="M15.75 5.25a3 3 0 0 1 3 3m3 0a6 6 0 0 1-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1 1 21.75 8.25Z"
|
||||
/>
|
||||
</svg>
|
||||
Authentication
|
||||
Security
|
||||
</AppRailAnchor>
|
||||
<AppRailAnchor href="/paths" selected={$page.url.pathname === '/paths'}>
|
||||
<svg
|
||||
@@ -124,7 +94,7 @@
|
||||
</svg>
|
||||
Paths
|
||||
</AppRailAnchor>
|
||||
<AppRailAnchor href="/stats" selected={$page.url.pathname === '/stats'}>
|
||||
<AppRailAnchor href="/webhooks" selected={$page.url.pathname === '/webhooks'}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
@@ -136,120 +106,35 @@
|
||||
<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"
|
||||
d="M12 21a9.004 9.004 0 0 0 8.716-6.747M12 21a9.004 9.004 0 0 1-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 0 1 7.843 4.582M12 3a8.997 8.997 0 0 0-7.843 4.582m15.686 0A11.953 11.953 0 0 1 12 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0 1 21 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0 1 12 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 0 1 3 12c0-1.605.42-3.113 1.157-4.418"
|
||||
/>
|
||||
</svg>
|
||||
Stats
|
||||
Webhooks
|
||||
</AppRailAnchor>
|
||||
<AppRailAnchor href="/components" selected={$page.url.pathname === '/components'}>
|
||||
<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="m20.25 7.5-.625 10.632a2.25 2.25 0 0 1-2.247 2.118H6.622a2.25 2.25 0 0 1-2.247-2.118L3.75 7.5M10 11.25h4M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125Z"
|
||||
/>
|
||||
</svg>
|
||||
Components
|
||||
</AppRailAnchor>
|
||||
|
||||
|
||||
<svelte:fragment slot="trail">
|
||||
<FileButton
|
||||
bind:files
|
||||
accept=".yml,.yaml,.json"
|
||||
button="btn text-sm rounded-none text-wrap variant-soft-primary flex flex-col justify-center items-center h-20 w-full"
|
||||
on:change={onFileUpload}
|
||||
type="file"
|
||||
name="openapispec"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="size-6"
|
||||
>
|
||||
<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>
|
||||
Upload
|
||||
</FileButton>
|
||||
<button
|
||||
type="button"
|
||||
class="btn text-sm rounded-none text-wrap variant-soft-primary flex flex-col justify-center items-center h-20 w-full"
|
||||
on:click={() => {
|
||||
const openApiStorage = localStorage.getItem(`${localStoragePrefix}openApi`);
|
||||
if (!openApiStorage) return;
|
||||
const openApi = JSON.parse(openApiStorage);
|
||||
const blob = new Blob([JSON.stringify(openApi, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${fileName}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="size-6"
|
||||
>
|
||||
<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.25m.75 12 3 3m0 0 3-3m-3 3v-6m-1.5-9H5.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>
|
||||
Download JSON
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn text-sm rounded-none text-wrap variant-soft-primary flex flex-col justify-center items-center h-20 w-full"
|
||||
on:click={() => {
|
||||
const openApiStorage = localStorage.getItem(`${localStoragePrefix}openApi`);
|
||||
if (!openApiStorage) return;
|
||||
const openApi = JSON.parse(openApiStorage);
|
||||
const blob = new Blob(
|
||||
[stringify(openApi, null, { indent: 2, aliasDuplicateObjects: false })],
|
||||
{ type: 'application/yaml' }
|
||||
);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${fileName}.yaml`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="size-6"
|
||||
>
|
||||
<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.25m.75 12 3 3m0 0 3-3m-3 3v-6m-1.5-9H5.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>
|
||||
Download YAML
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn text-sm rounded-none text-wrap variant-soft-error hover:variant-soft-error flex justify-center items-center h-16 w-full"
|
||||
on:click={() => {
|
||||
if (confirm('Are you sure you want to reset ALL current inputs?')) {
|
||||
// remove `openApi` from localStorage
|
||||
localStorage.removeItem(`${localStoragePrefix}openApi`);
|
||||
window.location.pathname = '/';
|
||||
}
|
||||
}}
|
||||
>
|
||||
Clear all inputs
|
||||
</button>
|
||||
<div class="flex justify-center items-center h-10 w-full my-4">
|
||||
<div class="p-2">
|
||||
<DownloadButtons />
|
||||
</div>
|
||||
<div class="flex justify-center my-4">
|
||||
<LightSwitch />
|
||||
</div>
|
||||
<AppRailAnchor href="https://www.speakeasyapi.dev/openapi" target="_blank">
|
||||
|
||||
7
src/routes/components/+page.svelte
Normal file
7
src/routes/components/+page.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
</script>
|
||||
|
||||
components/+page.svelte
|
||||
@@ -3,3 +3,5 @@
|
||||
</script>
|
||||
|
||||
<Info />
|
||||
|
||||
<!-- TODO: Tags and External Documentation -->
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { HttpMethods, openApiStore } from '$lib';
|
||||
import { HttpMethods } from '$lib';
|
||||
import ParameterInput from '$lib/components/atoms/ParameterInput.svelte';
|
||||
import { getPathVariables } from '$lib/pathHandling';
|
||||
import type { OpenAPIV3 } from '$lib/openAPITypes';
|
||||
import type { OpenAPIV3_1 } from '$lib/openAPITypes';
|
||||
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;
|
||||
|
||||
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 tempPath: OpenAPIV3.PathItemObject = {
|
||||
let tempPath: OpenAPIV3_1.PathItemObject = {
|
||||
parameters: []
|
||||
};
|
||||
openApiStore.subscribe((store) => {
|
||||
selectedSpec.subscribe((store) => {
|
||||
if (!data.pathName) return;
|
||||
if (store.paths == undefined) tempPath = {};
|
||||
if (!store.paths!.hasOwnProperty(data.pathName)) tempPath = {};
|
||||
if (store.spec.paths == undefined) tempPath = {};
|
||||
if (!store.spec.paths!.hasOwnProperty(data.pathName)) tempPath = {};
|
||||
// @ts-expect-error - working with a known not empty object
|
||||
tempPath = store.paths[data.pathName] ?? {};
|
||||
|
||||
@@ -96,9 +101,11 @@
|
||||
<h4 class="h4">Parameters</h4>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="content">
|
||||
{#each tempPath.parameters as param}
|
||||
<ParameterInput variableName={param.name} bind:value={param} location="path" />
|
||||
{/each}
|
||||
{#if tempPath.parameters}
|
||||
{#each tempPath.parameters.filter(filterParams) as param}
|
||||
<ParameterInput variableName={param.name} bind:value={param} location="path" />
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<span class="flex items-center gap-2">
|
||||
<select name="newParameter" bind:value={newParam} class="select w-min">
|
||||
@@ -128,7 +135,8 @@
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
on:input={(event) => {
|
||||
if (event.target.checked) {
|
||||
//@ts-expect-error - working with a known object
|
||||
if (event.target?.checked) {
|
||||
tempPath[method] = {
|
||||
tags: [],
|
||||
summary: '',
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import type { PageLoad } from './$types';
|
||||
import { openApiStore } from '$lib';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import type { OpenAPIV3_1 } from '$lib/openAPITypes';
|
||||
import { selectedSpec } from '$lib/db';
|
||||
|
||||
export const load = (async (event) => {
|
||||
// check if path param is an integer
|
||||
if (!/^\d+$/.test(event.params.index)) error(404, 'Invalid path index');
|
||||
const index = parseInt(event.params.index);
|
||||
let apiObject: OpenAPIV3_1.Document;
|
||||
openApiStore.subscribe((value) => (apiObject = value));
|
||||
selectedSpec.subscribe((value) => (apiObject = value.spec));
|
||||
let pathName: string | undefined;
|
||||
// @ts-expect-error - svelte stores populate the value
|
||||
if (apiObject && !apiObject.paths) error(404, 'No paths found in the OpenAPI document');
|
||||
|
||||
@@ -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>
|
||||
7
src/routes/webhooks/+page.svelte
Normal file
7
src/routes/webhooks/+page.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
</script>
|
||||
|
||||
webhooks/+page.svelte
|
||||
Reference in New Issue
Block a user