@sanity/workbench 0.1.0-alpha.2 → 0.1.0-alpha.20

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 (48) hide show
  1. package/README.md +24 -0
  2. package/dist/_chunks-es/index.js +39 -0
  3. package/dist/_chunks-es/index.js.map +1 -0
  4. package/dist/_chunks-es/studio.js +864 -0
  5. package/dist/_chunks-es/studio.js.map +1 -0
  6. package/dist/_internal.d.ts +16 -4
  7. package/dist/_internal.js +34 -26
  8. package/dist/_internal.js.map +1 -1
  9. package/dist/core.d.ts +2138 -0
  10. package/dist/core.js +75 -0
  11. package/dist/core.js.map +1 -0
  12. package/dist/system.d.ts +2120 -0
  13. package/dist/system.js +888 -0
  14. package/dist/system.js.map +1 -0
  15. package/package.json +33 -6
  16. package/src/_exports/core.ts +1 -0
  17. package/src/_exports/system.ts +1 -0
  18. package/src/_internal/index.ts +2 -1
  19. package/src/_internal/render.ts +72 -42
  20. package/src/core/applications/application-list.ts +104 -0
  21. package/src/core/applications/application.ts +174 -0
  22. package/src/core/canvases.ts +92 -0
  23. package/src/core/config.ts +34 -0
  24. package/src/core/env.ts +43 -0
  25. package/src/core/index.ts +13 -0
  26. package/src/core/log/index.ts +125 -0
  27. package/src/core/media-libraries.ts +93 -0
  28. package/src/core/organizations.ts +115 -0
  29. package/src/core/projects.ts +114 -0
  30. package/src/core/shared/urls.ts +129 -0
  31. package/src/core/user-applications/core-app.ts +148 -0
  32. package/src/core/user-applications/studios/index.ts +3 -0
  33. package/src/core/user-applications/studios/schemas.ts +128 -0
  34. package/src/core/user-applications/studios/studio.ts +533 -0
  35. package/src/core/user-applications/studios/workspace.ts +152 -0
  36. package/src/core/user-applications/user-application.ts +222 -0
  37. package/src/system/auth.machine.ts +223 -0
  38. package/src/system/index.ts +22 -0
  39. package/src/system/inspect.ts +40 -0
  40. package/src/system/load-federated-module.ts +54 -0
  41. package/src/system/remote.machine.ts +219 -0
  42. package/src/system/remotes.machine.ts +92 -0
  43. package/src/system/root.machine.ts +224 -0
  44. package/src/system/service.machine.ts +207 -0
  45. package/src/system/services.machine.ts +120 -0
  46. package/src/system/system-preferences.machine.ts +215 -0
  47. package/src/system/telemetry.machine.ts +179 -0
  48. package/src/_internal/render.test.ts +0 -18
@@ -0,0 +1,174 @@
1
+ import {
2
+ type App,
3
+ type Interface,
4
+ InterfaceSchema,
5
+ type LocalApp,
6
+ type LocalInterface,
7
+ LocalInterfaceSchema,
8
+ type LocalPanel,
9
+ type LocalService,
10
+ type Panel,
11
+ type PanelComponent,
12
+ type Service,
13
+ } from "@sanity/federation";
14
+ import type { Resource as ProtocolResource } from "@sanity/message-protocol";
15
+
16
+ // The interface model lives in @sanity/federation, which owns the
17
+ // `unstable_defineView` / `unstable_defineService` contracts. Re-export it so it
18
+ // stays part of the workbench app-model surface. `Panel`/`LocalPanel`,
19
+ // `Service`/`LocalService`, and `App`/`LocalApp` are the panel-, worker-, and
20
+ // app-typed subsets.
21
+ export {
22
+ type App,
23
+ type Interface,
24
+ InterfaceSchema,
25
+ type LocalApp,
26
+ type LocalInterface,
27
+ LocalInterfaceSchema,
28
+ type LocalPanel,
29
+ type LocalService,
30
+ type Panel,
31
+ type PanelComponent,
32
+ type Service,
33
+ };
34
+
35
+ /**
36
+ * @public
37
+ */
38
+ export type AbstractApplicationType =
39
+ | "studio"
40
+ | "coreApp"
41
+ | "canvas"
42
+ | "media-library"
43
+ | "workspace";
44
+
45
+ /**
46
+ * @public
47
+ */
48
+ export abstract class AbstractApplication<
49
+ TType extends AbstractApplicationType,
50
+ TProtocolResource extends ProtocolResource = Extract<
51
+ ProtocolResource,
52
+ { type: TType }
53
+ >,
54
+ > {
55
+ readonly type: TType;
56
+
57
+ constructor(type: TType) {
58
+ this.type = type;
59
+ }
60
+
61
+ /**
62
+ * The in-app route this application navigates to, or `null` when it isn't
63
+ * navigable as a full-page app (US5 — e.g. an SDK app with no `app` view).
64
+ * The dock renders a navigable item only for a non-null `href`; the app
65
+ * routes 404 a `null`-href app on direct visit.
66
+ */
67
+ abstract get href(): string | null;
68
+
69
+ abstract get title(): string;
70
+
71
+ abstract get id(): string;
72
+
73
+ /**
74
+ * Whether the application is federated or not. This is used to determine
75
+ * if the application should be rendered in an iframe or not.
76
+ */
77
+ abstract get isFederated(): boolean;
78
+
79
+ /**
80
+ * Whether the application is served by a local CLI dev server rather than a
81
+ * deployed remote. Defaults to `false`; user applications flip this when
82
+ * constructed from a `LocalUserApplication` payload.
83
+ */
84
+ get isLocal(): boolean {
85
+ return false;
86
+ }
87
+
88
+ abstract get url(): URL;
89
+
90
+ /**
91
+ * Interfaces the application exposes (e.g. dock panels). Defaults to none;
92
+ * user applications surface what they declared via `unstable_defineApp`.
93
+ */
94
+ get interfaces(): readonly LocalInterface[] {
95
+ return [];
96
+ }
97
+
98
+ /**
99
+ * Whether the application has a navigable full-page `app` view (US5). Defaults
100
+ * to `true` — studios, Canvas, and Media Library are always navigable. An SDK
101
+ * app overrides this to derive it from whether it declares an `app` interface.
102
+ */
103
+ get hasAppView(): boolean {
104
+ return true;
105
+ }
106
+
107
+ /**
108
+ * Federation module id of the app's full-page view (`${id}/App`), or `null`
109
+ * when it can't be federation-loaded: the app isn't federated (Canvas, Media
110
+ * Library, deployed apps shown in an iframe) or has no app view. Only a
111
+ * non-null `moduleId` is fetched and rendered by the remotes machine.
112
+ */
113
+ get moduleId(): string | null {
114
+ return this.isFederated && this.hasAppView ? `${this.id}/App` : null;
115
+ }
116
+
117
+ /**
118
+ * Federation module id of one of this app's panel view components, or `null`
119
+ * when the app isn't federated (a non-federated app's panels can't be loaded
120
+ * as remotes).
121
+ */
122
+ resolveViewModuleId(
123
+ view: LocalPanel,
124
+ component: PanelComponent,
125
+ ): string | null {
126
+ return this.isFederated
127
+ ? `${this.id}/views/${view.name}/${component}`
128
+ : null;
129
+ }
130
+
131
+ get initials(): string {
132
+ const SYMBOLS = /[^\p{Alpha}\p{N}\p{White_Space}]/gu;
133
+ const WHITESPACE = /\p{White_Space}+/u;
134
+ const ALPHANUMERIC_SEGMENTS = /(\p{N}+|\p{Alpha}+)/gu;
135
+ const IS_NUMERIC = /^\p{N}+$/u;
136
+
137
+ if (!this.title) return "";
138
+
139
+ const namesArray = this.title
140
+ .replace(SYMBOLS, "")
141
+ .split(WHITESPACE)
142
+ .filter(Boolean);
143
+
144
+ if (namesArray.length === 0) return "";
145
+
146
+ if (namesArray.length === 1) {
147
+ const word = namesArray[0];
148
+ const segments = word.match(ALPHANUMERIC_SEGMENTS) || [];
149
+
150
+ if (segments.length === 0) return "";
151
+ if (segments.length === 1) {
152
+ if (word.length === 1) {
153
+ return word.toUpperCase();
154
+ }
155
+
156
+ if (IS_NUMERIC.test(word)) {
157
+ return `${word.charAt(0)}${word.charAt(1)}`.toUpperCase();
158
+ }
159
+
160
+ return word.charAt(0).toUpperCase();
161
+ }
162
+
163
+ return `${segments[0]!.charAt(0)}${segments[1].charAt(0)}`.toUpperCase();
164
+ }
165
+
166
+ return `${namesArray[0].charAt(0)}${namesArray[namesArray.length - 1].charAt(0)}`.toUpperCase();
167
+ }
168
+
169
+ /**
170
+ * Converts the resource to a protocol resource that comlink expects
171
+ * for backwards compatibility with the old API format.
172
+ */
173
+ abstract toProtocolResource(): TProtocolResource;
174
+ }
@@ -0,0 +1,92 @@
1
+ import type { CanvasResource as ProtocolCanvasResource } from "@sanity/message-protocol";
2
+ import { z } from "zod";
3
+
4
+ import { AbstractApplication } from "./applications/application";
5
+ import { getSanityDomain } from "./env";
6
+ import { OrganizationId } from "./organizations";
7
+
8
+ /**
9
+ * Canvas ID schema, branded for type safety.
10
+ * @public
11
+ */
12
+ const CanvasId = z.string().nonempty().brand("CanvasId");
13
+
14
+ /**
15
+ * Canvas ID type, branded for type safety.
16
+ * @public
17
+ */
18
+ export type CanvasId = z.output<typeof CanvasId>;
19
+
20
+ /**
21
+ * Validates and brands a string as a CanvasId.
22
+ * @public
23
+ */
24
+ export function brandCanvasId(id: string): CanvasId {
25
+ return CanvasId.parse(id);
26
+ }
27
+
28
+ const Canvas = z.object({
29
+ id: CanvasId,
30
+ organizationId: OrganizationId,
31
+ status: z.enum(["active", "provisioning"]),
32
+ });
33
+
34
+ /**
35
+ * Represents a Canvas resource as returned from the API.
36
+ * @public
37
+ */
38
+ export type Canvas = z.output<typeof Canvas>;
39
+
40
+ /**
41
+ * Validates and parses a raw API response into a branded
42
+ * Canvas.
43
+ * @public
44
+ */
45
+ export function parseCanvas(data: unknown): Canvas {
46
+ return Canvas.parse(data);
47
+ }
48
+
49
+ /**
50
+ * Whilst the constructor takes an organization's canvas resource the existance
51
+ * therefore implies the organization has access to the canvas application which
52
+ * is what workbench most importantly wants to know.
53
+ * @public
54
+ */
55
+ export class CanvasApplication extends AbstractApplication<"canvas"> {
56
+ readonly canvas: Canvas;
57
+
58
+ constructor(canvas: Canvas) {
59
+ super("canvas");
60
+
61
+ this.canvas = canvas;
62
+ }
63
+
64
+ get id(): CanvasId {
65
+ return this.canvas.id;
66
+ }
67
+
68
+ get href(): string {
69
+ return "canvas";
70
+ }
71
+
72
+ get title(): string {
73
+ return "Canvas";
74
+ }
75
+
76
+ get url(): URL {
77
+ return new URL(
78
+ `https://canvas.${getSanityDomain()}/o/${this.canvas.organizationId}`,
79
+ );
80
+ }
81
+
82
+ get isFederated(): boolean {
83
+ return false;
84
+ }
85
+
86
+ toProtocolResource(): ProtocolCanvasResource {
87
+ return {
88
+ type: "canvas",
89
+ id: this.id,
90
+ } satisfies ProtocolCanvasResource;
91
+ }
92
+ }
@@ -0,0 +1,34 @@
1
+ import type { OrganizationId } from "./organizations";
2
+
3
+ /**
4
+ * Workbench configuration.
5
+ *
6
+ * @public
7
+ */
8
+ interface Config {
9
+ /**
10
+ * The organization ID to use when rendering the workbench.
11
+ */
12
+ organizationId?: string;
13
+ }
14
+
15
+ /**
16
+ * The resolved configuration after processing
17
+ * and validating the provided config.
18
+ *
19
+ * @public
20
+ */
21
+ type ResolvedConfig = Omit<Config, "organizationId"> & {
22
+ organizationId: OrganizationId;
23
+ };
24
+
25
+ /**
26
+ * Options for rendering a remote module, such as a user application.
27
+ *
28
+ * @public
29
+ */
30
+ interface RemoteModuleRenderOptions {
31
+ reactStrictMode?: boolean;
32
+ }
33
+
34
+ export type { Config, ResolvedConfig, RemoteModuleRenderOptions };
@@ -0,0 +1,43 @@
1
+ declare const __SANITY_STAGING__: boolean | undefined;
2
+
3
+ /**
4
+ * Returns the Sanity domain based on the `__SANITY_STAGING__` runtime-time flag.
5
+ * If the flag is set to `true`, the staging domain is returned.
6
+ * Otherwise, the production domain is returned.
7
+ *
8
+ * @public
9
+ */
10
+ export function getSanityDomain(): string {
11
+ if (getSanityEnv() === "staging") {
12
+ return "sanity.work";
13
+ }
14
+ return "sanity.io";
15
+ }
16
+
17
+ /**
18
+ * Returns the API host based on the `__SANITY_STAGING__` runtime-time flag.
19
+ * If the flag is set to `true`, the staging API host is returned.
20
+ * Otherwise, the production API host is returned.
21
+ *
22
+ * @public
23
+ */
24
+ export function getApiHost(): string | undefined {
25
+ return `https://api.${getSanityDomain()}`;
26
+ }
27
+
28
+ /**
29
+ * Returns the current Sanity environment based on the `__SANITY_STAGING__` runtime-time flag.
30
+ * If the flag is set to `true`, "staging" is returned.
31
+ * Otherwise, "production" is returned.
32
+ *
33
+ * @public
34
+ */
35
+ export function getSanityEnv(): "staging" | "production" {
36
+ if (
37
+ typeof __SANITY_STAGING__ !== "undefined" &&
38
+ __SANITY_STAGING__ === true
39
+ ) {
40
+ return "staging";
41
+ }
42
+ return "production";
43
+ }
@@ -0,0 +1,13 @@
1
+ export * from "./applications/application";
2
+ export * from "./applications/application-list";
3
+ export * from "./canvases";
4
+ export * from "./config";
5
+ export * from "./env";
6
+ export * from "./log";
7
+ export * from "./media-libraries";
8
+ export * from "./organizations";
9
+ export * from "./projects";
10
+ export { joinUrlPaths } from "./shared/urls";
11
+ export * from "./user-applications/core-app";
12
+ export * from "./user-applications/studios";
13
+ export * from "./user-applications/user-application";
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Log levels in order of verbosity (least to most)
3
+ * - none: Silent
4
+ * - error: Critical failures that prevent operation
5
+ * - warn: Issues that may cause problems but don't stop execution
6
+ * - info: High-level informational messages (default)
7
+ * - debug: Detailed debugging information (maintainer level)
8
+ * - trace: Very detailed tracing — sets `internal: true` on context
9
+ * @public
10
+ */
11
+ export type LogLevel = "none" | "error" | "warn" | "info" | "debug";
12
+
13
+ /**
14
+ * Namespaces organize logs by functional domain.
15
+ * @internal
16
+ */
17
+ export type LogNamespace = string;
18
+
19
+ type LogContext = { [key: string]: unknown };
20
+
21
+ /**
22
+ * @public
23
+ */
24
+ export interface Logger {
25
+ error: (message: string, context?: LogContext) => void;
26
+ warn: (message: string, context?: LogContext) => void;
27
+ info: (message: string, context?: LogContext) => void;
28
+ debug: (message: string, context?: LogContext) => void;
29
+ child: (domain: string, context?: LogContext) => Logger;
30
+ }
31
+
32
+ const LEVELS: readonly LogLevel[] = ["none", "error", "warn", "info", "debug"];
33
+
34
+ interface LoggerOptions {
35
+ namespace?: LogNamespace;
36
+ context?: LogContext;
37
+ logLevel?: LogLevel;
38
+ }
39
+
40
+ /**
41
+ * Creates a leveled logger with an optional namespace prefix and bound
42
+ * context. Calls below the configured `logLevel` are suppressed; `"none"`
43
+ * silences the logger entirely.
44
+ *
45
+ * Use {@link Logger.child} to derive a sub-logger with an extended namespace
46
+ * (e.g. `parent:domain`) that inherits the parent's level and merges its
47
+ * bound context.
48
+ *
49
+ * @param options - Logger configuration.
50
+ * @param options.namespace - Prepended to every message in `[brackets]`.
51
+ * @param options.context - Bound context merged with per-call context.
52
+ * Per-call keys win on conflict.
53
+ * @param options.logLevel - Maximum verbosity to emit. Default `"info"`.
54
+ *
55
+ * @example
56
+ * ```ts
57
+ * const log = createLogger({ namespace: "checkout", logLevel: "debug" });
58
+ * log.info("placed", { orderId: "abc" });
59
+ * // → [checkout] placed { orderId: "abc" }
60
+ *
61
+ * const auth = log.child("auth", { tenant: "acme" });
62
+ * auth.warn("token expiring");
63
+ * // → [checkout:auth] token expiring { tenant: "acme" }
64
+ * ```
65
+ *
66
+ * @public
67
+ */
68
+ export function createLogger({
69
+ namespace,
70
+ context: baseContext,
71
+ logLevel = "info",
72
+ }: LoggerOptions = {}): Logger {
73
+ function isLevelEnabled(level: LogLevel): boolean {
74
+ return LEVELS.indexOf(level) <= LEVELS.indexOf(logLevel);
75
+ }
76
+
77
+ function logAtLevel(
78
+ level: LogLevel,
79
+ message: string,
80
+ context?: LogContext,
81
+ ): void {
82
+ if (!isLevelEnabled(level)) return;
83
+
84
+ const merged =
85
+ (baseContext ?? context) ? { ...baseContext, ...context } : undefined;
86
+ const args: unknown[] = [
87
+ ...(namespace ? [`[${namespace}]`] : []),
88
+ message,
89
+ ...(merged ? [merged] : []),
90
+ ];
91
+
92
+ if (level === "error") console.error(...args);
93
+ else if (level === "warn") console.warn(...args);
94
+ // oxlint-disable-next-line no-console
95
+ else if (level === "info") console.info(...args);
96
+ // oxlint-disable-next-line no-console
97
+ else console.debug(...args);
98
+ }
99
+
100
+ return {
101
+ error: (message, context) => logAtLevel("error", message, context),
102
+ warn: (message, context) => logAtLevel("warn", message, context),
103
+ info: (message, context) => logAtLevel("info", message, context),
104
+ debug: (message, context) => logAtLevel("debug", message, context),
105
+ child: (domain, context) =>
106
+ createLogger({
107
+ logLevel,
108
+ namespace: namespace ? `${namespace}:${domain}` : domain,
109
+ context:
110
+ baseContext || context ? { ...baseContext, ...context } : undefined,
111
+ }),
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Shared workbench logger instance. Use this from both the workbench host
117
+ * and its remotes so lifecycle and diagnostic logs appear under a single
118
+ * namespace.
119
+ *
120
+ * @public
121
+ */
122
+ export const logger: Logger = createLogger({
123
+ namespace: "sanity-workbench",
124
+ logLevel: "debug",
125
+ });
@@ -0,0 +1,93 @@
1
+ import type { MediaResource as ProtocolMediaResource } from "@sanity/message-protocol";
2
+ import { z } from "zod";
3
+
4
+ import { AbstractApplication } from "./applications/application";
5
+ import { getSanityDomain } from "./env";
6
+ import { OrganizationId } from "./organizations";
7
+
8
+ /**
9
+ * Canvas ID schema, branded for type safety.
10
+ * @public
11
+ */
12
+ const MediaLibraryId = z.string().nonempty().brand("MediaLibraryId");
13
+
14
+ /**
15
+ * MediaLibrary ID type, branded for type safety.
16
+ * @public
17
+ */
18
+ export type MediaLibraryId = z.output<typeof MediaLibraryId>;
19
+
20
+ /**
21
+ * Validates and brands a string as a MediaLibraryId.
22
+ * @public
23
+ */
24
+ export function brandMediaLibraryId(id: string): MediaLibraryId {
25
+ return MediaLibraryId.parse(id);
26
+ }
27
+
28
+ const MediaLibrary = z.object({
29
+ id: MediaLibraryId,
30
+ organizationId: OrganizationId,
31
+ status: z.enum(["active", "provisioning"]),
32
+ aclMode: z.enum(["private", "public"]),
33
+ });
34
+
35
+ /**
36
+ * Represents a MediaLibrary resource as returned from the API.
37
+ * @public
38
+ */
39
+ export type MediaLibrary = z.output<typeof MediaLibrary>;
40
+
41
+ /**
42
+ * Validates and parses a raw API response into a branded
43
+ * MediaLibrary.
44
+ * @public
45
+ */
46
+ export function parseMediaLibrary(data: unknown): MediaLibrary {
47
+ return MediaLibrary.parse(data);
48
+ }
49
+
50
+ /**
51
+ * Whilst the constructor takes an organization's media library resource the existance
52
+ * therefore implies the organization has access to the media library application which
53
+ * is what workbench most importantly wants to know.
54
+ * @public
55
+ */
56
+ export class MediaLibraryApplication extends AbstractApplication<"media-library"> {
57
+ readonly library: MediaLibrary;
58
+
59
+ constructor(library: MediaLibrary) {
60
+ super("media-library");
61
+
62
+ this.library = library;
63
+ }
64
+
65
+ get id(): string {
66
+ return this.library.id;
67
+ }
68
+
69
+ get href(): string {
70
+ return "media";
71
+ }
72
+
73
+ get title(): string {
74
+ return "Media Library";
75
+ }
76
+
77
+ get isFederated(): boolean {
78
+ return false;
79
+ }
80
+
81
+ get url(): URL {
82
+ return new URL(
83
+ `https://media.${getSanityDomain()}/${this.library.organizationId}`,
84
+ );
85
+ }
86
+
87
+ toProtocolResource(): ProtocolMediaResource {
88
+ return {
89
+ type: "media-library",
90
+ id: this.id,
91
+ } satisfies ProtocolMediaResource;
92
+ }
93
+ }
@@ -0,0 +1,115 @@
1
+ import { z } from "zod";
2
+
3
+ /**
4
+ * Organization ID schema, branded for type safety.
5
+ * @public
6
+ */
7
+ export const OrganizationId = z.string().nonempty().brand("OrganizationId");
8
+
9
+ /**
10
+ * Organization ID type, branded for type safety.
11
+ * @public
12
+ */
13
+ export type OrganizationId = z.output<typeof OrganizationId>;
14
+
15
+ /**
16
+ * Validates and brands a string as an OrganizationId.
17
+ * @public
18
+ */
19
+ export function brandOrganizationId(id: string): OrganizationId {
20
+ return OrganizationId.parse(id);
21
+ }
22
+
23
+ const OrganizationMember = z.object({
24
+ sanityUserId: z.string(),
25
+ isCurrentUser: z.boolean(),
26
+ user: z.object({
27
+ id: z.string(),
28
+ displayName: z.string(),
29
+ familyName: z.string(),
30
+ givenName: z.string(),
31
+ middleName: z.string().nullable(),
32
+ imageUrl: z.string().nullable(),
33
+ email: z.string(),
34
+ loginProvider: z.string(),
35
+ }),
36
+ roles: z.array(
37
+ z.object({
38
+ name: z.string(),
39
+ title: z.string(),
40
+ description: z.string().optional(),
41
+ }),
42
+ ),
43
+ });
44
+
45
+ /**
46
+ * @public
47
+ */
48
+ export type OrganizationMember = z.output<typeof OrganizationMember>;
49
+
50
+ /**
51
+ * Organization schema — validates and brands API responses
52
+ * from the `/organizations/:id` endpoint.
53
+ * @public
54
+ */
55
+ export const Organization = z.object({
56
+ id: OrganizationId,
57
+ name: z.string(),
58
+ slug: z.string().nullable(),
59
+ createdAt: z.string(),
60
+ updatedAt: z.string(),
61
+ dashboardStatus: z.enum(["enabled", "disabled"]),
62
+ aiFeaturesStatus: z.enum(["enabled", "disabled"]),
63
+ defaultRoleName: z.string().nullable(),
64
+ });
65
+
66
+ /**
67
+ * Represents an organization with optional members and
68
+ * features arrays depending on the generic parameters.
69
+ * - `Organization` — base fields only (default)
70
+ * - `Organization<true>` — includes `members`
71
+ * - `Organization<true, true>` — includes both
72
+ * @public
73
+ */
74
+ export type Organization<
75
+ IncludeMembers extends boolean = true,
76
+ IncludeFeatures extends boolean = true,
77
+ > = z.output<typeof Organization> &
78
+ (IncludeMembers extends true ? { members: OrganizationMember[] } : unknown) &
79
+ (IncludeFeatures extends true ? { features: string[] } : unknown);
80
+
81
+ /**
82
+ * Validates and parses a raw API response into a branded
83
+ * Organization. The options control which schema is used —
84
+ * matching what the API returns based on query params.
85
+ * @public
86
+ */
87
+ export function parseOrganization<
88
+ IncludeMembers extends boolean = true,
89
+ IncludeFeatures extends boolean = true,
90
+ >(
91
+ data: unknown,
92
+ options?: {
93
+ includeMembers?: IncludeMembers;
94
+ includeFeatures?: IncludeFeatures;
95
+ },
96
+ ): Organization<IncludeMembers, IncludeFeatures> {
97
+ const includeMembers = options?.includeMembers ?? true;
98
+ const includeFeatures = options?.includeFeatures ?? true;
99
+
100
+ const extensions = {
101
+ ...(includeMembers && {
102
+ members: z.array(OrganizationMember),
103
+ }),
104
+ ...(includeFeatures && {
105
+ features: z.array(z.string()),
106
+ }),
107
+ };
108
+
109
+ const schema =
110
+ Object.keys(extensions).length > 0
111
+ ? Organization.extend(extensions)
112
+ : Organization;
113
+
114
+ return schema.parse(data) as Organization<IncludeMembers, IncludeFeatures>;
115
+ }