@nwire/studio 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +72 -0
- package/components.json +19 -0
- package/index.html +12 -0
- package/package.json +66 -0
- package/src/App.vue +305 -0
- package/src/components/EmptyState.stories.ts +53 -0
- package/src/components/EmptyState.vue +28 -0
- package/src/components/ErrorBoundary.vue +60 -0
- package/src/components/FilterInput.stories.ts +32 -0
- package/src/components/FilterInput.vue +33 -0
- package/src/components/JsonView.stories.ts +38 -0
- package/src/components/JsonView.vue +34 -0
- package/src/components/KindBadge.stories.ts +72 -0
- package/src/components/KindBadge.vue +59 -0
- package/src/components/ListRow.stories.ts +56 -0
- package/src/components/ListRow.vue +48 -0
- package/src/components/MasterDetail.stories.ts +74 -0
- package/src/components/MasterDetail.vue +35 -0
- package/src/components/MonacoViewer.vue +143 -0
- package/src/components/PageHeader.stories.ts +45 -0
- package/src/components/PageHeader.vue +46 -0
- package/src/components/SchemaNode.vue +208 -0
- package/src/components/SchemaTree.vue +65 -0
- package/src/components/SourceDrawer.vue +136 -0
- package/src/components/SourcePill.vue +103 -0
- package/src/components/__tests__/EmptyState.test.ts +28 -0
- package/src/components/__tests__/ErrorBoundary.test.ts +52 -0
- package/src/components/__tests__/FilterInput.test.ts +38 -0
- package/src/components/__tests__/JsonView.test.ts +33 -0
- package/src/components/__tests__/KindBadge.test.ts +39 -0
- package/src/components/__tests__/ListRow.test.ts +39 -0
- package/src/components/__tests__/MasterDetail.test.ts +40 -0
- package/src/components/__tests__/PageHeader.test.ts +42 -0
- package/src/components/index.ts +17 -0
- package/src/components/ui/badge/Badge.vue +17 -0
- package/src/components/ui/badge/index.ts +25 -0
- package/src/components/ui/button/Button.vue +28 -0
- package/src/components/ui/button/index.ts +34 -0
- package/src/components/ui/card/Card.vue +14 -0
- package/src/components/ui/card/CardContent.vue +14 -0
- package/src/components/ui/card/CardDescription.vue +14 -0
- package/src/components/ui/card/CardFooter.vue +14 -0
- package/src/components/ui/card/CardHeader.vue +14 -0
- package/src/components/ui/card/CardTitle.vue +14 -0
- package/src/components/ui/card/index.ts +6 -0
- package/src/components/ui/dialog/Dialog.vue +15 -0
- package/src/components/ui/dialog/DialogClose.vue +12 -0
- package/src/components/ui/dialog/DialogContent.vue +47 -0
- package/src/components/ui/dialog/DialogDescription.vue +22 -0
- package/src/components/ui/dialog/DialogFooter.vue +12 -0
- package/src/components/ui/dialog/DialogHeader.vue +14 -0
- package/src/components/ui/dialog/DialogScrollContent.vue +60 -0
- package/src/components/ui/dialog/DialogTitle.vue +22 -0
- package/src/components/ui/dialog/DialogTrigger.vue +12 -0
- package/src/components/ui/dialog/index.ts +9 -0
- package/src/components/ui/input/Input.vue +32 -0
- package/src/components/ui/input/index.ts +1 -0
- package/src/components/ui/scroll-area/ScrollArea.vue +22 -0
- package/src/components/ui/scroll-area/ScrollBar.vue +32 -0
- package/src/components/ui/scroll-area/index.ts +2 -0
- package/src/components/ui/separator/Separator.vue +27 -0
- package/src/components/ui/separator/index.ts +1 -0
- package/src/components/ui/skeleton/Skeleton.vue +14 -0
- package/src/components/ui/skeleton/index.ts +1 -0
- package/src/components/ui/tabs/Tabs.vue +15 -0
- package/src/components/ui/tabs/TabsContent.vue +25 -0
- package/src/components/ui/tabs/TabsList.vue +25 -0
- package/src/components/ui/tabs/TabsTrigger.vue +29 -0
- package/src/components/ui/tabs/index.ts +4 -0
- package/src/components/ui/tooltip/Tooltip.vue +15 -0
- package/src/components/ui/tooltip/TooltipContent.vue +40 -0
- package/src/components/ui/tooltip/TooltipProvider.vue +12 -0
- package/src/components/ui/tooltip/TooltipTrigger.vue +12 -0
- package/src/components/ui/tooltip/index.ts +4 -0
- package/src/composables/useCopy.ts +31 -0
- package/src/lib/__tests__/normalize-cache.test.ts +104 -0
- package/src/lib/cache.ts +334 -0
- package/src/lib/normalize-cache.ts +92 -0
- package/src/lib/project-catalog.ts +125 -0
- package/src/lib/utils.ts +6 -0
- package/src/main.ts +112 -0
- package/src/pages/Actions.vue +180 -0
- package/src/pages/Commands.vue +262 -0
- package/src/pages/Dispatch.vue +431 -0
- package/src/pages/Events.vue +166 -0
- package/src/pages/Home.stories.ts +47 -0
- package/src/pages/Home.vue +485 -0
- package/src/pages/Hooks.vue +297 -0
- package/src/pages/Live.vue +249 -0
- package/src/pages/Modules.vue +174 -0
- package/src/pages/Overview.vue +159 -0
- package/src/pages/Plugins.stories.ts +44 -0
- package/src/pages/Plugins.vue +403 -0
- package/src/pages/Projects.vue +272 -0
- package/src/pages/Run.vue +479 -0
- package/src/pages/Topology.vue +164 -0
- package/src/pages/Trace.vue +511 -0
- package/src/pages/TraceNode.vue +166 -0
- package/src/pages/Workflows.vue +191 -0
- package/src/pages/__tests__/Actions.test.ts +98 -0
- package/src/pages/__tests__/Home.test.ts +98 -0
- package/src/pages/__tests__/Hooks.test.ts +119 -0
- package/src/pages/__tests__/Plugins.test.ts +80 -0
- package/src/style.css +40 -0
- package/tsconfig.json +20 -0
- package/vite.config.ts +892 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as Input } from "./Input.vue";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { ScrollAreaRootProps } from "reka-ui";
|
|
3
|
+
import type { HTMLAttributes } from "vue";
|
|
4
|
+
import { reactiveOmit } from "@vueuse/core";
|
|
5
|
+
import { ScrollAreaCorner, ScrollAreaRoot, ScrollAreaViewport } from "reka-ui";
|
|
6
|
+
import { cn } from "@/lib/utils";
|
|
7
|
+
import ScrollBar from "./ScrollBar.vue";
|
|
8
|
+
|
|
9
|
+
const props = defineProps<ScrollAreaRootProps & { class?: HTMLAttributes["class"] }>();
|
|
10
|
+
|
|
11
|
+
const delegatedProps = reactiveOmit(props, "class");
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<ScrollAreaRoot v-bind="delegatedProps" :class="cn('relative overflow-hidden', props.class)">
|
|
16
|
+
<ScrollAreaViewport class="h-full w-full rounded-[inherit]">
|
|
17
|
+
<slot />
|
|
18
|
+
</ScrollAreaViewport>
|
|
19
|
+
<ScrollBar />
|
|
20
|
+
<ScrollAreaCorner />
|
|
21
|
+
</ScrollAreaRoot>
|
|
22
|
+
</template>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { ScrollAreaScrollbarProps } from "reka-ui";
|
|
3
|
+
import type { HTMLAttributes } from "vue";
|
|
4
|
+
import { reactiveOmit } from "@vueuse/core";
|
|
5
|
+
import { ScrollAreaScrollbar, ScrollAreaThumb } from "reka-ui";
|
|
6
|
+
import { cn } from "@/lib/utils";
|
|
7
|
+
|
|
8
|
+
const props = withDefaults(
|
|
9
|
+
defineProps<ScrollAreaScrollbarProps & { class?: HTMLAttributes["class"] }>(),
|
|
10
|
+
{
|
|
11
|
+
orientation: "vertical",
|
|
12
|
+
},
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
const delegatedProps = reactiveOmit(props, "class");
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<ScrollAreaScrollbar
|
|
20
|
+
v-bind="delegatedProps"
|
|
21
|
+
:class="
|
|
22
|
+
cn(
|
|
23
|
+
'flex touch-none select-none transition-colors',
|
|
24
|
+
orientation === 'vertical' && 'h-full w-2.5 border-l border-l-transparent p-px',
|
|
25
|
+
orientation === 'horizontal' && 'h-2.5 flex-col border-t border-t-transparent p-px',
|
|
26
|
+
props.class,
|
|
27
|
+
)
|
|
28
|
+
"
|
|
29
|
+
>
|
|
30
|
+
<ScrollAreaThumb class="relative flex-1 rounded-full bg-border" />
|
|
31
|
+
</ScrollAreaScrollbar>
|
|
32
|
+
</template>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { SeparatorProps } from "reka-ui";
|
|
3
|
+
import type { HTMLAttributes } from "vue";
|
|
4
|
+
import { reactiveOmit } from "@vueuse/core";
|
|
5
|
+
import { Separator } from "reka-ui";
|
|
6
|
+
import { cn } from "@/lib/utils";
|
|
7
|
+
|
|
8
|
+
const props = withDefaults(defineProps<SeparatorProps & { class?: HTMLAttributes["class"] }>(), {
|
|
9
|
+
orientation: "horizontal",
|
|
10
|
+
decorative: true,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const delegatedProps = reactiveOmit(props, "class");
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<template>
|
|
17
|
+
<Separator
|
|
18
|
+
v-bind="delegatedProps"
|
|
19
|
+
:class="
|
|
20
|
+
cn(
|
|
21
|
+
'shrink-0 bg-border',
|
|
22
|
+
props.orientation === 'horizontal' ? 'h-px w-full' : 'w-px h-full',
|
|
23
|
+
props.class,
|
|
24
|
+
)
|
|
25
|
+
"
|
|
26
|
+
/>
|
|
27
|
+
</template>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as Separator } from "./Separator.vue";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from "vue";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
interface SkeletonProps {
|
|
6
|
+
class?: HTMLAttributes["class"];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const props = defineProps<SkeletonProps>();
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<template>
|
|
13
|
+
<div :class="cn('animate-pulse rounded-md bg-muted', props.class)" />
|
|
14
|
+
</template>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as Skeleton } from "./Skeleton.vue";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { TabsRootEmits, TabsRootProps } from "reka-ui";
|
|
3
|
+
import { TabsRoot, useForwardPropsEmits } from "reka-ui";
|
|
4
|
+
|
|
5
|
+
const props = defineProps<TabsRootProps>();
|
|
6
|
+
const emits = defineEmits<TabsRootEmits>();
|
|
7
|
+
|
|
8
|
+
const forwarded = useForwardPropsEmits(props, emits);
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<template>
|
|
12
|
+
<TabsRoot v-bind="forwarded">
|
|
13
|
+
<slot />
|
|
14
|
+
</TabsRoot>
|
|
15
|
+
</template>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { TabsContentProps } from "reka-ui";
|
|
3
|
+
import type { HTMLAttributes } from "vue";
|
|
4
|
+
import { reactiveOmit } from "@vueuse/core";
|
|
5
|
+
import { TabsContent } from "reka-ui";
|
|
6
|
+
import { cn } from "@/lib/utils";
|
|
7
|
+
|
|
8
|
+
const props = defineProps<TabsContentProps & { class?: HTMLAttributes["class"] }>();
|
|
9
|
+
|
|
10
|
+
const delegatedProps = reactiveOmit(props, "class");
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<template>
|
|
14
|
+
<TabsContent
|
|
15
|
+
:class="
|
|
16
|
+
cn(
|
|
17
|
+
'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
|
18
|
+
props.class,
|
|
19
|
+
)
|
|
20
|
+
"
|
|
21
|
+
v-bind="delegatedProps"
|
|
22
|
+
>
|
|
23
|
+
<slot />
|
|
24
|
+
</TabsContent>
|
|
25
|
+
</template>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { TabsListProps } from "reka-ui";
|
|
3
|
+
import type { HTMLAttributes } from "vue";
|
|
4
|
+
import { reactiveOmit } from "@vueuse/core";
|
|
5
|
+
import { TabsList } from "reka-ui";
|
|
6
|
+
import { cn } from "@/lib/utils";
|
|
7
|
+
|
|
8
|
+
const props = defineProps<TabsListProps & { class?: HTMLAttributes["class"] }>();
|
|
9
|
+
|
|
10
|
+
const delegatedProps = reactiveOmit(props, "class");
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<template>
|
|
14
|
+
<TabsList
|
|
15
|
+
v-bind="delegatedProps"
|
|
16
|
+
:class="
|
|
17
|
+
cn(
|
|
18
|
+
'inline-flex items-center justify-center rounded-md bg-muted p-1 text-muted-foreground',
|
|
19
|
+
props.class,
|
|
20
|
+
)
|
|
21
|
+
"
|
|
22
|
+
>
|
|
23
|
+
<slot />
|
|
24
|
+
</TabsList>
|
|
25
|
+
</template>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { TabsTriggerProps } from "reka-ui";
|
|
3
|
+
import type { HTMLAttributes } from "vue";
|
|
4
|
+
import { reactiveOmit } from "@vueuse/core";
|
|
5
|
+
import { TabsTrigger, useForwardProps } from "reka-ui";
|
|
6
|
+
import { cn } from "@/lib/utils";
|
|
7
|
+
|
|
8
|
+
const props = defineProps<TabsTriggerProps & { class?: HTMLAttributes["class"] }>();
|
|
9
|
+
|
|
10
|
+
const delegatedProps = reactiveOmit(props, "class");
|
|
11
|
+
|
|
12
|
+
const forwardedProps = useForwardProps(delegatedProps);
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<template>
|
|
16
|
+
<TabsTrigger
|
|
17
|
+
v-bind="forwardedProps"
|
|
18
|
+
:class="
|
|
19
|
+
cn(
|
|
20
|
+
'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm',
|
|
21
|
+
props.class,
|
|
22
|
+
)
|
|
23
|
+
"
|
|
24
|
+
>
|
|
25
|
+
<span class="truncate">
|
|
26
|
+
<slot />
|
|
27
|
+
</span>
|
|
28
|
+
</TabsTrigger>
|
|
29
|
+
</template>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { TooltipRootEmits, TooltipRootProps } from "reka-ui";
|
|
3
|
+
import { TooltipRoot, useForwardPropsEmits } from "reka-ui";
|
|
4
|
+
|
|
5
|
+
const props = defineProps<TooltipRootProps>();
|
|
6
|
+
const emits = defineEmits<TooltipRootEmits>();
|
|
7
|
+
|
|
8
|
+
const forwarded = useForwardPropsEmits(props, emits);
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<template>
|
|
12
|
+
<TooltipRoot v-bind="forwarded">
|
|
13
|
+
<slot />
|
|
14
|
+
</TooltipRoot>
|
|
15
|
+
</template>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { TooltipContentEmits, TooltipContentProps } from "reka-ui";
|
|
3
|
+
import type { HTMLAttributes } from "vue";
|
|
4
|
+
import { reactiveOmit } from "@vueuse/core";
|
|
5
|
+
import { TooltipContent, TooltipPortal, useForwardPropsEmits } from "reka-ui";
|
|
6
|
+
import { cn } from "@/lib/utils";
|
|
7
|
+
|
|
8
|
+
defineOptions({
|
|
9
|
+
inheritAttrs: false,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const props = withDefaults(
|
|
13
|
+
defineProps<TooltipContentProps & { class?: HTMLAttributes["class"] }>(),
|
|
14
|
+
{
|
|
15
|
+
sideOffset: 4,
|
|
16
|
+
},
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const emits = defineEmits<TooltipContentEmits>();
|
|
20
|
+
|
|
21
|
+
const delegatedProps = reactiveOmit(props, "class");
|
|
22
|
+
|
|
23
|
+
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<template>
|
|
27
|
+
<TooltipPortal>
|
|
28
|
+
<TooltipContent
|
|
29
|
+
v-bind="{ ...forwarded, ...$attrs }"
|
|
30
|
+
:class="
|
|
31
|
+
cn(
|
|
32
|
+
'z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
|
33
|
+
props.class,
|
|
34
|
+
)
|
|
35
|
+
"
|
|
36
|
+
>
|
|
37
|
+
<slot />
|
|
38
|
+
</TooltipContent>
|
|
39
|
+
</TooltipPortal>
|
|
40
|
+
</template>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { TooltipProviderProps } from "reka-ui";
|
|
3
|
+
import { TooltipProvider } from "reka-ui";
|
|
4
|
+
|
|
5
|
+
const props = defineProps<TooltipProviderProps>();
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<template>
|
|
9
|
+
<TooltipProvider v-bind="props">
|
|
10
|
+
<slot />
|
|
11
|
+
</TooltipProvider>
|
|
12
|
+
</template>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { TooltipTriggerProps } from "reka-ui";
|
|
3
|
+
import { TooltipTrigger } from "reka-ui";
|
|
4
|
+
|
|
5
|
+
const props = defineProps<TooltipTriggerProps>();
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<template>
|
|
9
|
+
<TooltipTrigger v-bind="props">
|
|
10
|
+
<slot />
|
|
11
|
+
</TooltipTrigger>
|
|
12
|
+
</template>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { ref } from "vue";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Clipboard helper with a 1.5s "copied" flash for the UI.
|
|
5
|
+
*
|
|
6
|
+
* const { copy, copied } = useCopy();
|
|
7
|
+
* copy("hello"); // → fires navigator.clipboard.writeText
|
|
8
|
+
* // copied.value flips to true for 1500ms
|
|
9
|
+
*/
|
|
10
|
+
export function useCopy(flashMs = 1500) {
|
|
11
|
+
const copied = ref(false);
|
|
12
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
13
|
+
|
|
14
|
+
async function copy(text: string): Promise<void> {
|
|
15
|
+
try {
|
|
16
|
+
if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
|
|
17
|
+
await navigator.clipboard.writeText(text);
|
|
18
|
+
}
|
|
19
|
+
copied.value = true;
|
|
20
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
21
|
+
timeoutId = setTimeout(() => {
|
|
22
|
+
copied.value = false;
|
|
23
|
+
}, flashMs);
|
|
24
|
+
} catch {
|
|
25
|
+
// Clipboard may be unavailable (iframe, insecure context, denied
|
|
26
|
+
// permission). Silently fail — the chip just won't flash.
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return { copy, copied };
|
|
31
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { normalizeCache } from "../normalize-cache";
|
|
3
|
+
|
|
4
|
+
describe("normalizeCache", () => {
|
|
5
|
+
it("fills every expected array when input is empty", () => {
|
|
6
|
+
const { cache, missingFields } = normalizeCache({});
|
|
7
|
+
expect(cache).not.toBeNull();
|
|
8
|
+
expect(cache?.apps).toEqual([]);
|
|
9
|
+
expect(cache?.modules).toEqual([]);
|
|
10
|
+
expect(cache?.actions).toEqual([]);
|
|
11
|
+
expect(cache?.events).toEqual([]);
|
|
12
|
+
expect(cache?.actors).toEqual([]);
|
|
13
|
+
expect(cache?.projections).toEqual([]);
|
|
14
|
+
expect(cache?.queries).toEqual([]);
|
|
15
|
+
expect(cache?.resolvers).toEqual([]);
|
|
16
|
+
expect(cache?.routes).toEqual([]);
|
|
17
|
+
expect(cache?.workflows).toEqual([]);
|
|
18
|
+
expect(cache?.graph).toEqual({ events: [] });
|
|
19
|
+
expect(missingFields).toContain("resolvers");
|
|
20
|
+
expect(missingFields).toContain("workflows");
|
|
21
|
+
expect(missingFields).toContain("graph");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("preserves arrays that ARE present", () => {
|
|
25
|
+
const input = {
|
|
26
|
+
apps: [{ name: "x", modules: [] }],
|
|
27
|
+
modules: [],
|
|
28
|
+
actions: [],
|
|
29
|
+
events: [],
|
|
30
|
+
actors: [],
|
|
31
|
+
projections: [],
|
|
32
|
+
queries: [],
|
|
33
|
+
resolvers: [{ operation: "Op", version: 1 }],
|
|
34
|
+
routes: [],
|
|
35
|
+
workflows: [],
|
|
36
|
+
externalCalls: [],
|
|
37
|
+
inboundWebhooks: [],
|
|
38
|
+
outboxes: [],
|
|
39
|
+
inboxes: [],
|
|
40
|
+
crons: [],
|
|
41
|
+
hooks: [],
|
|
42
|
+
plugins: [],
|
|
43
|
+
graph: { events: [] },
|
|
44
|
+
generatedAt: "2026-05-17T00:00:00Z",
|
|
45
|
+
};
|
|
46
|
+
const { cache, missingFields } = normalizeCache(input);
|
|
47
|
+
expect(cache?.apps).toEqual(input.apps);
|
|
48
|
+
expect(cache?.resolvers).toEqual(input.resolvers);
|
|
49
|
+
expect(missingFields).toEqual([]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("reports the exact list of missing array fields", () => {
|
|
53
|
+
const { missingFields } = normalizeCache({
|
|
54
|
+
apps: [],
|
|
55
|
+
modules: [],
|
|
56
|
+
actions: [],
|
|
57
|
+
events: [],
|
|
58
|
+
actors: [],
|
|
59
|
+
projections: [],
|
|
60
|
+
queries: [],
|
|
61
|
+
// resolvers + workflows + graph missing
|
|
62
|
+
routes: [],
|
|
63
|
+
externalCalls: [],
|
|
64
|
+
inboundWebhooks: [],
|
|
65
|
+
outboxes: [],
|
|
66
|
+
inboxes: [],
|
|
67
|
+
crons: [],
|
|
68
|
+
generatedAt: "2026-05-17T00:00:00Z",
|
|
69
|
+
});
|
|
70
|
+
expect([...missingFields].sort()).toEqual([
|
|
71
|
+
"graph",
|
|
72
|
+
"hooks",
|
|
73
|
+
"plugins",
|
|
74
|
+
"resolvers",
|
|
75
|
+
"workflows",
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("ignores non-array values masquerading as the field", () => {
|
|
80
|
+
const { cache, missingFields } = normalizeCache({ resolvers: "not an array" });
|
|
81
|
+
expect(cache?.resolvers).toEqual([]);
|
|
82
|
+
expect(missingFields).toContain("resolvers");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("returns null + fatal error when input is not an object", () => {
|
|
86
|
+
expect(normalizeCache(null).cache).toBeNull();
|
|
87
|
+
expect(normalizeCache(null).fatalError).toMatch(/not a JSON object/i);
|
|
88
|
+
expect(normalizeCache([]).cache).toBeNull();
|
|
89
|
+
expect(normalizeCache("oops").cache).toBeNull();
|
|
90
|
+
expect(normalizeCache(42).cache).toBeNull();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("synthesises generatedAt when missing", () => {
|
|
94
|
+
const { cache, missingFields } = normalizeCache({});
|
|
95
|
+
expect(cache?.generatedAt).toBeTypeOf("string");
|
|
96
|
+
expect(missingFields).toContain("generatedAt");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("handles graph.events missing while graph exists", () => {
|
|
100
|
+
const { cache, missingFields } = normalizeCache({ graph: {} });
|
|
101
|
+
expect(cache?.graph).toEqual({ events: [] });
|
|
102
|
+
expect(missingFields).toContain("graph.events");
|
|
103
|
+
});
|
|
104
|
+
});
|