@shortlink-org/portolan 0.3.0 → 0.4.0

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.
Files changed (188) hide show
  1. package/README.md +151 -6
  2. package/catalog/model.go +147 -0
  3. package/catalog/roundtrip_test.go +7 -0
  4. package/cli/portolan.mjs +6 -1
  5. package/go.mod +7 -0
  6. package/go.sum +20 -6
  7. package/package.json +10 -9
  8. package/plugin/describe.go +38 -0
  9. package/plugins/README.md +296 -5
  10. package/plugins/extract-argocd/options.schema.json +44 -0
  11. package/plugins/extract-celery/main.py +1 -0
  12. package/plugins/extract-csharp-ddd/README.md +213 -0
  13. package/plugins/extract-csharp-ddd/options.schema.json +55 -0
  14. package/plugins/extract-django/README.md +13 -2
  15. package/plugins/extract-django/choices.py +47 -0
  16. package/plugins/extract-django/domain.py +2 -1
  17. package/plugins/extract-django/extract_test.py +13 -0
  18. package/plugins/extract-django/lifecycle.py +2 -28
  19. package/plugins/extract-django/main.py +1 -0
  20. package/plugins/extract-django/rules.py +154 -0
  21. package/plugins/extract-django/rules_test.py +158 -0
  22. package/plugins/extract-go-sqs/options.schema.json +21 -0
  23. package/plugins/extract-http-clients/describe.go +5 -4
  24. package/plugins/extract-java/build/org/portolan/extract/Main.class +0 -0
  25. package/plugins/extract-k8s/options.schema.json +32 -0
  26. package/plugins/extract-laravel/Cargo.lock +962 -0
  27. package/plugins/extract-laravel/Cargo.toml +20 -0
  28. package/plugins/extract-laravel/README.md +200 -0
  29. package/plugins/extract-laravel/options.schema.json +68 -0
  30. package/plugins/extract-laravel/rustfmt.toml +1 -0
  31. package/plugins/extract-php-ddd/Cargo.lock +972 -0
  32. package/plugins/extract-php-ddd/Cargo.toml +22 -0
  33. package/plugins/extract-php-ddd/README.md +141 -0
  34. package/plugins/extract-php-ddd/options.schema.json +50 -0
  35. package/plugins/extract-php-ddd/rustfmt.toml +1 -0
  36. package/plugins/extract-python-kafka/main.py +1 -0
  37. package/plugins/extract-terraform/options.schema.json +26 -0
  38. package/plugins/extract-ts/main.ts +1 -0
  39. package/plugins/phpscan/Cargo.lock +953 -0
  40. package/plugins/phpscan/Cargo.toml +19 -0
  41. package/plugins/phpscan/rustfmt.toml +1 -0
  42. package/plugins/portolan-go.wasm +0 -0
  43. package/plugins/pyplugin/catalog.py +24 -1
  44. package/plugins/verify-otel/options.schema.json +12 -0
  45. package/portolan.json +130 -3
  46. package/rules/builtin.json +218 -0
  47. package/schema/portolan.schema.json +880 -13
  48. package/scripts/README.md +3 -1
  49. package/scripts/delivery-presets.mjs +36 -3
  50. package/scripts/gen-likec4.mjs +149 -16
  51. package/scripts/gen-likec4.test.mjs +96 -0
  52. package/scripts/gen.mjs +30 -3
  53. package/scripts/gitops-example.test.mjs +108 -0
  54. package/scripts/host-plugins/fetch-argocd.mjs +338 -0
  55. package/scripts/host-plugins/fetch-argocd.options.json +49 -0
  56. package/scripts/host-plugins/fetch-argocd.test.mjs +274 -0
  57. package/scripts/host-plugins/fetch-bsr.mjs +1 -0
  58. package/scripts/host-plugins/fetch-csr.mjs +1 -0
  59. package/scripts/host-plugins/fetch-git.mjs +1 -0
  60. package/scripts/host-plugins/fetch-k8s.mjs +263 -0
  61. package/scripts/host-plugins/fetch-k8s.options.json +50 -0
  62. package/scripts/host-plugins/fetch-k8s.test.mjs +259 -0
  63. package/scripts/host-plugins/k8s-topology.mjs +183 -0
  64. package/scripts/json-format.mjs +192 -0
  65. package/scripts/json-format.test.mjs +97 -0
  66. package/scripts/local-api.mjs +216 -8
  67. package/scripts/local-api.test.mjs +88 -2
  68. package/scripts/local-discovery.mjs +12 -0
  69. package/scripts/manifest.mjs +10 -1
  70. package/scripts/plugin-host.mjs +23 -1
  71. package/scripts/plugins-fresh.mjs +97 -0
  72. package/scripts/plugins-fresh.test.mjs +64 -0
  73. package/scripts/run-builtin.mjs +26 -5
  74. package/scripts/schema.mjs +138 -19
  75. package/scripts/trace-trials.mjs +176 -0
  76. package/scripts/trace-trials.test.mjs +142 -0
  77. package/scripts/warning-policy.mjs +12 -6
  78. package/scripts/warning-policy.test.mjs +40 -3
  79. package/src/app/Breadcrumbs.test.ts +3 -0
  80. package/src/app/Breadcrumbs.tsx +3 -0
  81. package/src/app/CatalogApp.tsx +2 -0
  82. package/src/app/SidebarFooter.tsx +20 -4
  83. package/src/catalog-index.ts +25 -0
  84. package/src/catalog-model.ts +246 -1
  85. package/src/catalog-validation.ts +104 -2
  86. package/src/catalog.test.ts +40 -1
  87. package/src/components/ChannelRows.test.tsx +76 -1
  88. package/src/components/ChannelRows.tsx +223 -60
  89. package/src/components/DeploymentRows.tsx +131 -0
  90. package/src/components/DocsLinks.test.tsx +27 -0
  91. package/src/components/DocsLinks.tsx +56 -0
  92. package/src/components/FieldTree.tsx +5 -2
  93. package/src/components/HTTPDestinationEvidence.tsx +1 -1
  94. package/src/components/PluginIcon.tsx +77 -0
  95. package/src/components/ProblemRow.tsx +173 -153
  96. package/src/components/RuleMarks.tsx +22 -0
  97. package/src/components/ShapeRows.tsx +24 -20
  98. package/src/data.ts +21 -0
  99. package/src/enrich.test.ts +127 -3
  100. package/src/enrich.ts +104 -6
  101. package/src/er/ErCanvas.tsx +218 -12
  102. package/src/er/GroupNode.tsx +57 -0
  103. package/src/er/StoreHeader.tsx +1 -0
  104. package/src/er/layout.test.ts +85 -2
  105. package/src/er/layout.ts +140 -5
  106. package/src/er/spec.test.ts +17 -0
  107. package/src/er/spec.ts +23 -10
  108. package/src/flow/Recordings.test.tsx +52 -0
  109. package/src/flow/Recordings.tsx +236 -0
  110. package/src/flow/StepDetail.tsx +53 -0
  111. package/src/flow/TraceTrial.tsx +419 -0
  112. package/src/flow/examples.test.ts +33 -0
  113. package/src/flow/examples.ts +37 -0
  114. package/src/flow/outline.test.ts +28 -0
  115. package/src/flow/outline.ts +7 -2
  116. package/src/flow/trace-trial-resume.test.ts +42 -0
  117. package/src/flow/trace-trial-resume.ts +74 -0
  118. package/src/graph/elk.ts +78 -0
  119. package/src/index.css +24 -0
  120. package/src/landing/LandingPage.tsx +15 -11
  121. package/src/landing/ProductTour.tsx +6 -6
  122. package/src/lib/all-problems.ts +27 -17
  123. package/src/lib/centrality.test.ts +251 -0
  124. package/src/lib/centrality.ts +232 -0
  125. package/src/lib/confluence.test.ts +37 -0
  126. package/src/lib/confluence.ts +41 -0
  127. package/src/lib/context-color.ts +1 -1
  128. package/src/lib/deployment-drift.ts +22 -0
  129. package/src/lib/derive.ts +43 -69
  130. package/src/lib/environments.test.ts +83 -0
  131. package/src/lib/environments.ts +57 -0
  132. package/src/lib/integration-url.test.ts +30 -0
  133. package/src/lib/integration-url.ts +63 -0
  134. package/src/lib/kafka-ui.ts +3 -45
  135. package/src/lib/local-api.ts +96 -2
  136. package/src/lib/notion.ts +13 -0
  137. package/src/lib/plugin-index.json +2765 -0
  138. package/src/lib/plugins.test.ts +68 -0
  139. package/src/lib/plugins.ts +264 -0
  140. package/src/lib/problem-flows.test.ts +61 -0
  141. package/src/lib/problem-flows.ts +78 -0
  142. package/src/lib/problem-rules-cel.d.mts +43 -0
  143. package/src/lib/problem-rules-cel.mjs +407 -0
  144. package/src/lib/problem-rules.test.ts +287 -0
  145. package/src/lib/problem-rules.ts +271 -0
  146. package/src/lib/problem-subjects.ts +737 -0
  147. package/src/lib/rule-entries.ts +39 -0
  148. package/src/lib/{data-problems.test.ts → rules-data.test.ts} +18 -16
  149. package/src/lib/rules-deploy.test.ts +133 -0
  150. package/src/lib/{problems.test.ts → rules-edges.test.ts} +8 -3
  151. package/src/lib/{proto-problems.test.ts → rules-proto.test.ts} +9 -25
  152. package/src/lib/{wire-problems.test.ts → rules-wire.test.ts} +13 -11
  153. package/src/lib/rules.test.ts +51 -0
  154. package/src/lib/rules.ts +86 -0
  155. package/src/lib/shape.test.ts +32 -0
  156. package/src/lib/shape.ts +30 -6
  157. package/src/lib/tech.ts +16 -0
  158. package/src/lib/trace-project.test.ts +34 -0
  159. package/src/lib/trace-project.ts +50 -0
  160. package/src/lib/use-problems.ts +23 -0
  161. package/src/lib/warnings.test.ts +9 -0
  162. package/src/lib/warnings.ts +10 -10
  163. package/src/likec4/ids.test.ts +6 -2
  164. package/src/likec4/ids.ts +43 -0
  165. package/src/main.tsx +23 -0
  166. package/src/merge-deployments.test.ts +127 -0
  167. package/src/merge.test.ts +82 -0
  168. package/src/merge.ts +173 -9
  169. package/src/pages/AggregatePage.tsx +57 -7
  170. package/src/pages/ContextMap.tsx +45 -3
  171. package/src/pages/ContextPage.tsx +2 -0
  172. package/src/pages/EventPage.tsx +15 -5
  173. package/src/pages/FlowDetail.tsx +23 -2
  174. package/src/pages/GraphPage.tsx +40 -3
  175. package/src/pages/Overview.tsx +152 -12
  176. package/src/pages/PluginIndex.tsx +190 -0
  177. package/src/pages/Problems.tsx +396 -128
  178. package/src/pages/ServicePage.tsx +58 -2
  179. package/src/pages/Settings.tsx +28 -2
  180. package/src/pages/settings/AboutSettings.tsx +8 -1
  181. package/src/pages/settings/IntegrationsSettings.tsx +63 -17
  182. package/src/pages/settings/RecordingSettings.tsx +138 -0
  183. package/src/pages/settings/RulesSettings.tsx +825 -0
  184. package/src/routes.test.ts +9 -0
  185. package/src/routes.ts +22 -1
  186. package/src/lib/data-problems.ts +0 -314
  187. package/src/lib/proto-problems.ts +0 -237
  188. package/src/lib/wire-problems.ts +0 -342
@@ -0,0 +1,63 @@
1
+ // What every reader-side integration shares: a web address the reader typed
2
+ // in, kept in this browser and nowhere else.
3
+ //
4
+ // The value belongs to the reader, not to the catalog: two readers can point
5
+ // the same generated estate at different installations. It therefore lives in
6
+ // localStorage, like the editor and display preferences, and never reaches
7
+ // portolan.json or a page that is committed.
8
+
9
+ import { create } from "zustand";
10
+
11
+ /**
12
+ * A trimmed http(s) URL without its fragment and trailing slash, "" for an
13
+ * empty value, and null for anything that cannot be opened safely - a
14
+ * javascript: address, or words that are not a URL at all.
15
+ */
16
+ export function normalizeIntegrationUrl(value: string): string | null {
17
+ const clean = value.trim();
18
+ if (!clean) return "";
19
+ try {
20
+ const url = new URL(clean);
21
+ if (url.protocol !== "http:" && url.protocol !== "https:") return null;
22
+ url.hash = "";
23
+ return url.toString().replace(/\/$/, "");
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+
29
+ export interface IntegrationState {
30
+ url: string;
31
+ setUrl: (url: string) => void;
32
+ }
33
+
34
+ /** One store per integration, read from localStorage under its own key. */
35
+ export function createIntegrationStore(key: string) {
36
+ const read = (): string => {
37
+ try {
38
+ const value = localStorage.getItem(key) ?? "";
39
+ return normalizeIntegrationUrl(value) ?? "";
40
+ } catch {
41
+ return "";
42
+ }
43
+ };
44
+
45
+ const write = (value: string): void => {
46
+ try {
47
+ if (value) localStorage.setItem(key, value);
48
+ else localStorage.removeItem(key);
49
+ } catch {
50
+ /* private mode: keep the value for this session */
51
+ }
52
+ };
53
+
54
+ return create<IntegrationState>()((set) => ({
55
+ url: read(),
56
+ setUrl: (url) => {
57
+ const normalized = normalizeIntegrationUrl(url);
58
+ if (normalized === null) return;
59
+ write(normalized);
60
+ set({ url: normalized });
61
+ },
62
+ }));
63
+ }
@@ -7,24 +7,13 @@
7
7
  // installation URL remains useful and opens Kafka UI without inventing a
8
8
  // cluster name.
9
9
 
10
- import { create } from "zustand";
11
10
  import type { Catalog, Channel } from "../catalog";
12
11
  import { walkSteps } from "../catalog";
12
+ import { createIntegrationStore, normalizeIntegrationUrl } from "./integration-url";
13
13
 
14
14
  export const KAFKA_UI_KEY = "portolan.integrations.kafka-ui";
15
15
 
16
- export function normalizeKafkaUiUrl(value: string): string | null {
17
- const clean = value.trim();
18
- if (!clean) return "";
19
- try {
20
- const url = new URL(clean);
21
- if (url.protocol !== "http:" && url.protocol !== "https:") return null;
22
- url.hash = "";
23
- return url.toString().replace(/\/$/, "");
24
- } catch {
25
- return null;
26
- }
27
- }
16
+ export const normalizeKafkaUiUrl = normalizeIntegrationUrl;
28
17
 
29
18
  /** Build Kafbat's topic route when the configured URL names a cluster. */
30
19
  export function kafkaUiTopicUrl(
@@ -71,35 +60,4 @@ export function isKafkaChannel(
71
60
  || handoffChannels.has(channel.address);
72
61
  }
73
62
 
74
- function read(): string {
75
- try {
76
- const value = localStorage.getItem(KAFKA_UI_KEY) ?? "";
77
- return normalizeKafkaUiUrl(value) ?? "";
78
- } catch {
79
- return "";
80
- }
81
- }
82
-
83
- function write(value: string): void {
84
- try {
85
- if (value) localStorage.setItem(KAFKA_UI_KEY, value);
86
- else localStorage.removeItem(KAFKA_UI_KEY);
87
- } catch {
88
- /* private mode: keep the value for this session */
89
- }
90
- }
91
-
92
- interface KafkaUiState {
93
- url: string;
94
- setUrl: (url: string) => void;
95
- }
96
-
97
- export const useKafkaUi = create<KafkaUiState>()((set) => ({
98
- url: read(),
99
- setUrl: (url) => {
100
- const normalized = normalizeKafkaUiUrl(url);
101
- if (normalized === null) return;
102
- write(normalized);
103
- set({ url: normalized });
104
- },
105
- }));
63
+ export const useKafkaUi = createIntegrationStore(KAFKA_UI_KEY);
@@ -1,5 +1,6 @@
1
1
  import type { SetupDiagnostic, SetupInfo, SetupPhase, SetupProject, SetupRunStepStatus } from "./setup-info";
2
2
  import type { DjangoAggregateCandidates } from "./django-aggregates";
3
+ import type { ProblemRuleEntry } from "./problem-rules";
3
4
 
4
5
  const ROOT = `${import.meta.env.BASE_URL}__portolan`;
5
6
  const LOCAL_HEADER = { "Content-Type": "application/json", "X-Portolan-Local": "1" };
@@ -130,13 +131,60 @@ export interface ProjectTrial {
130
131
  previewError?: string;
131
132
  }
132
133
 
134
+ /** One flow the verifier wrote after reading an uploaded recording. */
135
+ export interface TraceTrialFlow {
136
+ id: string;
137
+ slug: string;
138
+ name: string;
139
+ owner: string;
140
+ kind: "declared" | "observed";
141
+ /** Whether the uploaded recording itself showed this flow. */
142
+ inRecording: boolean;
143
+ /** Traces in the uploaded recording that showed it. */
144
+ traces: number;
145
+ verified: number;
146
+ unresolved: number;
147
+ /** Hops the recordings showed that the code does not declare. */
148
+ added: number;
149
+ /** Distinct steps the uploaded recording showed. */
150
+ shown: number;
151
+ steps: number;
152
+ examples: number;
153
+ }
154
+
155
+ export interface TraceTrialWarning {
156
+ kind: "service" | "event" | "route" | "call" | "channel" | "other";
157
+ /** The name the verifier could not place, for a service or an event. */
158
+ name?: string;
159
+ message: string;
160
+ }
161
+
162
+ /** What a trial run said about an uploaded recording. */
163
+ export interface TraceTrial {
164
+ project: string;
165
+ /** Where the recording lands, relative to the project root. */
166
+ recording: string;
167
+ /** Whether keeping it adds or widens the project's verify step. */
168
+ stepAdded: boolean;
169
+ /** What keeping it does to portolan.json: a new verify step, one more glob on the step it has, or nothing. */
170
+ stepChange: "added" | "widened" | "none";
171
+ status: SetupRunStepStatus;
172
+ spans: number;
173
+ flows: TraceTrialFlow[];
174
+ warnings: TraceTrialWarning[];
175
+ mappings: { services: Record<string, string>; events: Record<string, string>; routes: Record<string, string> };
176
+ }
177
+
178
+ export type RunMode = "write" | "check" | "preview" | "project-preview" | "trace-preview";
179
+
133
180
  export type RunEvent =
134
- | { type: "run-started"; at: string; runId: string; mode: "write" | "check" | "preview" | "project-preview" }
181
+ | { type: "run-started"; at: string; runId: string; mode: RunMode }
135
182
  | { type: "pipeline-ready"; at: string; stepCount: number }
136
183
  | { type: "step-started"; at: string; ordinal: number; phase: SetupPhase; plugin: string; input?: string; output: string }
137
184
  | { type: "step-finished"; at: string; ordinal: number; phase: SetupPhase; plugin: string; status: SetupRunStepStatus; durationMs: number; fileCount: number; changedCount: number; changes: Array<{ kind: "added" | "changed" | "removed"; path: string }>; files: string[]; warnings?: string[]; diagnostics?: SetupDiagnostic[]; message?: string }
138
185
  | { type: "preview-ready"; at: string; files: GeneratedFileDiff[]; totalFiles: number; truncated: boolean }
139
186
  | ({ type: "project-trial-ready"; at: string } & ProjectTrial)
187
+ | ({ type: "trace-trial-ready"; at: string } & TraceTrial)
140
188
  | { type: "run-finished"; at: string; status: string; durationMs?: number; message?: string }
141
189
  | { type: "process-finished"; at: string; status: string; durationMs?: number; message?: string }
142
190
  | { type: "log"; at: string; stream: "stdout" | "stderr"; message: string };
@@ -178,7 +226,7 @@ export class LocalApiError extends Error {
178
226
  }
179
227
  }
180
228
 
181
- export async function localStatus(): Promise<{ local: true; workspace: string; setup: SetupInfo; activeRun: { id: string; mode: "write" | "check" | "preview" } | null }> {
229
+ export async function localStatus(): Promise<{ local: true; workspace: string; setup: SetupInfo; activeRun: { id: string; mode: RunMode } | null }> {
182
230
  return json("/status");
183
231
  }
184
232
 
@@ -196,6 +244,21 @@ export async function saveDjangoAggregates(revision: string, selections: Array<{
196
244
  return json("/django-aggregates", { method: "POST", headers: LOCAL_HEADER, body: JSON.stringify({ revision, selections }) });
197
245
  }
198
246
 
247
+ /** The manifest's `problemRules`, with a revision a save must quote. */
248
+ export interface ProblemRulesState {
249
+ revision: string;
250
+ rules: ProblemRuleEntry[];
251
+ }
252
+
253
+ export async function problemRules(): Promise<ProblemRulesState> {
254
+ return json("/rules");
255
+ }
256
+
257
+ /** Replaces `problemRules` in portolan.json, once the server has type-checked every expression. */
258
+ export async function saveProblemRules(revision: string, rules: ProblemRuleEntry[]): Promise<ProblemRulesState> {
259
+ return json("/rules", { method: "POST", headers: LOCAL_HEADER, body: JSON.stringify({ revision, rules }) });
260
+ }
261
+
199
262
  export async function previewDeliveryPreset(provider?: DeliveryProvider, features?: DeliveryFeatureId[]): Promise<DeliveryPreset> {
200
263
  const query = new URLSearchParams();
201
264
  if (provider) query.set("provider", provider);
@@ -255,6 +318,37 @@ export async function disposeProjectTrial(runId: string): Promise<void> {
255
318
  await json(`/projects/trials/${encodeURIComponent(runId)}/dispose`, { method: "POST", headers: LOCAL_HEADER, body: "{}" });
256
319
  }
257
320
 
321
+ /**
322
+ * Uploads a recording and runs the generator over a copy of the workspace
323
+ * with it in place. The bytes go as they are; the project and the file's
324
+ * name go as headers, because a JSON envelope around 30 MB of spans is a
325
+ * copy nobody wants.
326
+ */
327
+ export async function startTraceTrial(file: File, projectId: string): Promise<{ runId: string; mode: "trace-preview"; recording: string; project: string; stepAdded: boolean; spans: number }> {
328
+ const response = await fetch(`${ROOT}/traces/trials`, {
329
+ method: "POST",
330
+ headers: {
331
+ "Content-Type": "application/octet-stream",
332
+ "X-Portolan-Local": "1",
333
+ "X-Portolan-Project": projectId,
334
+ "X-Portolan-Filename": encodeURIComponent(file.name),
335
+ },
336
+ body: file,
337
+ });
338
+ const value = await response.json() as { runId: string; mode: "trace-preview"; recording: string; project: string; stepAdded: boolean; spans: number; error?: string };
339
+ if (!response.ok) throw new LocalApiError(value.error || `Local API returned ${response.status}.`, { status: response.status });
340
+ return value;
341
+ }
342
+
343
+ /** Keeps the recording beside the project, with the names mapped, and regenerates when asked. */
344
+ export async function applyTraceTrial(runId: string, options: { generate: boolean; services?: Record<string, string>; events?: Record<string, string>; routes?: Record<string, string> }): Promise<{ recording: string; project: string; stepAdded: boolean; manifestChanged: boolean; undoToken: string | null; setup: SetupInfo; run: { runId: string; mode: "write" } | null }> {
345
+ return json(`/traces/trials/${encodeURIComponent(runId)}/apply`, { method: "POST", headers: LOCAL_HEADER, body: JSON.stringify(options) });
346
+ }
347
+
348
+ export async function disposeTraceTrial(runId: string): Promise<void> {
349
+ await json(`/traces/trials/${encodeURIComponent(runId)}/dispose`, { method: "POST", headers: LOCAL_HEADER, body: "{}" });
350
+ }
351
+
258
352
  export async function startGeneration(mode: "write" | "check" | "preview" = "preview", previewRunId?: string): Promise<{ runId: string; mode: "write" | "check" | "preview" }> {
259
353
  return json("/runs", { method: "POST", headers: LOCAL_HEADER, body: JSON.stringify({ mode, previewRunId }) });
260
354
  }
@@ -0,0 +1,13 @@
1
+ // The optional hand-off from the catalog to the Notion workspace where a
2
+ // team writes.
3
+ //
4
+ // Notion has no search address a link can open, so this is the workspace or
5
+ // teamspace itself: a page says where the documentation lives without
6
+ // pretending to know the page. An exact page would need the catalog to carry
7
+ // its URL, which is a field, not a reader preference.
8
+
9
+ import { createIntegrationStore } from "./integration-url";
10
+
11
+ export const NOTION_KEY = "portolan.integrations.notion";
12
+
13
+ export const useNotion = createIntegrationStore(NOTION_KEY);