@sanity/workbench 0.1.0-alpha.1 → 0.1.0-alpha.10
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/dist/_chunks-es/index.js +34 -0
- package/dist/_chunks-es/index.js.map +1 -0
- package/dist/_internal.d.ts +38 -4
- package/dist/_internal.js +42 -26
- package/dist/_internal.js.map +1 -1
- package/dist/core.d.ts +1617 -0
- package/dist/core.js +779 -0
- package/dist/core.js.map +1 -0
- package/package.json +23 -8
- package/src/_exports/_internal.ts +1 -0
- package/src/_exports/core.ts +1 -0
- package/src/_internal/env.ts +32 -0
- package/src/_internal/index.ts +3 -1
- package/src/_internal/render.ts +82 -38
- package/src/core/applications/application-list.ts +104 -0
- package/src/core/applications/application.ts +95 -0
- package/src/core/canvases.ts +91 -0
- package/src/core/config.ts +34 -0
- package/src/core/index.ts +12 -0
- package/src/core/log/index.ts +92 -0
- package/src/core/media-libraries.ts +92 -0
- package/src/core/organizations.ts +115 -0
- package/src/core/projects.ts +114 -0
- package/src/core/shared/urls.ts +129 -0
- package/src/core/user-applications/core-app.ts +131 -0
- package/src/core/user-applications/studios/index.ts +3 -0
- package/src/core/user-applications/studios/schemas.ts +111 -0
- package/src/core/user-applications/studios/studio.ts +504 -0
- package/src/core/user-applications/studios/workspace.ts +147 -0
- package/src/core/user-applications/user-application.ts +181 -0
- package/src/vite-env.d.ts +8 -0
- package/dist/_internal.cjs +0 -38
- package/dist/_internal.cjs.map +0 -1
- package/dist/_internal.d.cts +0 -30
- package/src/env.d.ts +0 -7
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { CanvasResource as ProtocolCanvasResource } from "@sanity/message-protocol";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
import { AbstractApplication } from "./applications/application";
|
|
5
|
+
import { OrganizationId } from "./organizations";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Canvas ID schema, branded for type safety.
|
|
9
|
+
* @public
|
|
10
|
+
*/
|
|
11
|
+
const CanvasId = z.string().nonempty().brand("CanvasId");
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Canvas ID type, branded for type safety.
|
|
15
|
+
* @public
|
|
16
|
+
*/
|
|
17
|
+
export type CanvasId = z.output<typeof CanvasId>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Validates and brands a string as a CanvasId.
|
|
21
|
+
* @public
|
|
22
|
+
*/
|
|
23
|
+
export function brandCanvasId(id: string): CanvasId {
|
|
24
|
+
return CanvasId.parse(id);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const Canvas = z.object({
|
|
28
|
+
id: CanvasId,
|
|
29
|
+
organizationId: OrganizationId,
|
|
30
|
+
status: z.enum(["active", "provisioning"]),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Represents a Canvas resource as returned from the API.
|
|
35
|
+
* @public
|
|
36
|
+
*/
|
|
37
|
+
export type Canvas = z.output<typeof Canvas>;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Validates and parses a raw API response into a branded
|
|
41
|
+
* Canvas.
|
|
42
|
+
* @public
|
|
43
|
+
*/
|
|
44
|
+
export function parseCanvas(data: unknown): Canvas {
|
|
45
|
+
return Canvas.parse(data);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Whilst the constructor takes an organization's canvas resource the existance
|
|
50
|
+
* therefore implies the organization has access to the canvas application which
|
|
51
|
+
* is what workbench most importantly wants to know.
|
|
52
|
+
* @public
|
|
53
|
+
*/
|
|
54
|
+
export class CanvasApplication extends AbstractApplication<"canvas"> {
|
|
55
|
+
readonly canvas: Canvas;
|
|
56
|
+
|
|
57
|
+
constructor(canvas: Canvas) {
|
|
58
|
+
super("canvas");
|
|
59
|
+
|
|
60
|
+
this.canvas = canvas;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
get id(): CanvasId {
|
|
64
|
+
return this.canvas.id;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
get href(): string {
|
|
68
|
+
return "canvas";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
get title(): string {
|
|
72
|
+
return "Canvas";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
get url(): URL {
|
|
76
|
+
return new URL(
|
|
77
|
+
`https://canvas.${import.meta.env.VITE_SANITY_DOMAIN}/o/${this.canvas.organizationId}`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
get isFederated(): boolean {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
toProtocolResource(): ProtocolCanvasResource {
|
|
86
|
+
return {
|
|
87
|
+
type: "canvas",
|
|
88
|
+
id: this.id,
|
|
89
|
+
} satisfies ProtocolCanvasResource;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -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,12 @@
|
|
|
1
|
+
export * from "./applications/application";
|
|
2
|
+
export * from "./applications/application-list";
|
|
3
|
+
export * from "./canvases";
|
|
4
|
+
export * from "./config";
|
|
5
|
+
export * from "./log";
|
|
6
|
+
export * from "./media-libraries";
|
|
7
|
+
export * from "./organizations";
|
|
8
|
+
export * from "./projects";
|
|
9
|
+
export { joinUrlPaths } from "./shared/urls";
|
|
10
|
+
export * from "./user-applications/core-app";
|
|
11
|
+
export * from "./user-applications/studios";
|
|
12
|
+
export * from "./user-applications/user-application";
|
|
@@ -0,0 +1,92 @@
|
|
|
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
|
+
}
|
|
30
|
+
|
|
31
|
+
const LEVELS: readonly LogLevel[] = ["none", "error", "warn", "info", "debug"];
|
|
32
|
+
|
|
33
|
+
interface LoggerOptions {
|
|
34
|
+
namespace?: LogNamespace;
|
|
35
|
+
context?: LogContext;
|
|
36
|
+
logLevel?: LogLevel;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @public
|
|
41
|
+
*/
|
|
42
|
+
export function createLogger({
|
|
43
|
+
namespace,
|
|
44
|
+
context: baseContext,
|
|
45
|
+
logLevel = "info",
|
|
46
|
+
}: LoggerOptions = {}): Logger {
|
|
47
|
+
function isLevelEnabled(level: LogLevel): boolean {
|
|
48
|
+
return LEVELS.indexOf(level) <= LEVELS.indexOf(logLevel);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function logAtLevel(
|
|
52
|
+
level: LogLevel,
|
|
53
|
+
message: string,
|
|
54
|
+
context?: LogContext,
|
|
55
|
+
): void {
|
|
56
|
+
if (!isLevelEnabled(level)) return;
|
|
57
|
+
|
|
58
|
+
const merged =
|
|
59
|
+
(baseContext ?? context) ? { ...baseContext, ...context } : undefined;
|
|
60
|
+
const args: unknown[] = [
|
|
61
|
+
...(namespace ? [`[${namespace}]`] : []),
|
|
62
|
+
message,
|
|
63
|
+
...(merged ? [merged] : []),
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
if (level === "error") console.error(...args);
|
|
67
|
+
else if (level === "warn") console.warn(...args);
|
|
68
|
+
// oxlint-disable-next-line no-console
|
|
69
|
+
else if (level === "info") console.info(...args);
|
|
70
|
+
// oxlint-disable-next-line no-console
|
|
71
|
+
else console.debug(...args);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
error: (message, context) => logAtLevel("error", message, context),
|
|
76
|
+
warn: (message, context) => logAtLevel("warn", message, context),
|
|
77
|
+
info: (message, context) => logAtLevel("info", message, context),
|
|
78
|
+
debug: (message, context) => logAtLevel("debug", message, context),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Shared workbench logger instance. Use this from both the workbench host
|
|
84
|
+
* and its remotes so lifecycle and diagnostic logs appear under a single
|
|
85
|
+
* namespace.
|
|
86
|
+
*
|
|
87
|
+
* @public
|
|
88
|
+
*/
|
|
89
|
+
export const logger: Logger = createLogger({
|
|
90
|
+
namespace: "sanity-workbench",
|
|
91
|
+
logLevel: "debug",
|
|
92
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { MediaResource as ProtocolMediaResource } from "@sanity/message-protocol";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
import { AbstractApplication } from "./applications/application";
|
|
5
|
+
import { OrganizationId } from "./organizations";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Canvas ID schema, branded for type safety.
|
|
9
|
+
* @public
|
|
10
|
+
*/
|
|
11
|
+
const MediaLibraryId = z.string().nonempty().brand("MediaLibraryId");
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* MediaLibrary ID type, branded for type safety.
|
|
15
|
+
* @public
|
|
16
|
+
*/
|
|
17
|
+
export type MediaLibraryId = z.output<typeof MediaLibraryId>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Validates and brands a string as a MediaLibraryId.
|
|
21
|
+
* @public
|
|
22
|
+
*/
|
|
23
|
+
export function brandMediaLibraryId(id: string): MediaLibraryId {
|
|
24
|
+
return MediaLibraryId.parse(id);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const MediaLibrary = z.object({
|
|
28
|
+
id: MediaLibraryId,
|
|
29
|
+
organizationId: OrganizationId,
|
|
30
|
+
status: z.enum(["active", "provisioning"]),
|
|
31
|
+
aclMode: z.enum(["private", "public"]),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Represents a MediaLibrary resource as returned from the API.
|
|
36
|
+
* @public
|
|
37
|
+
*/
|
|
38
|
+
export type MediaLibrary = z.output<typeof MediaLibrary>;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Validates and parses a raw API response into a branded
|
|
42
|
+
* MediaLibrary.
|
|
43
|
+
* @public
|
|
44
|
+
*/
|
|
45
|
+
export function parseMediaLibrary(data: unknown): MediaLibrary {
|
|
46
|
+
return MediaLibrary.parse(data);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Whilst the constructor takes an organization's media library resource the existance
|
|
51
|
+
* therefore implies the organization has access to the media library application which
|
|
52
|
+
* is what workbench most importantly wants to know.
|
|
53
|
+
* @public
|
|
54
|
+
*/
|
|
55
|
+
export class MediaLibraryApplication extends AbstractApplication<"media-library"> {
|
|
56
|
+
readonly library: MediaLibrary;
|
|
57
|
+
|
|
58
|
+
constructor(library: MediaLibrary) {
|
|
59
|
+
super("media-library");
|
|
60
|
+
|
|
61
|
+
this.library = library;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
get id(): string {
|
|
65
|
+
return this.library.id;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get href(): string {
|
|
69
|
+
return "media";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get title(): string {
|
|
73
|
+
return "Media Library";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
get isFederated(): boolean {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
get url(): URL {
|
|
81
|
+
return new URL(
|
|
82
|
+
`https://media.${import.meta.env.VITE_SANITY_DOMAIN}/${this.library.organizationId}`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
toProtocolResource(): ProtocolMediaResource {
|
|
87
|
+
return {
|
|
88
|
+
type: "media-library",
|
|
89
|
+
id: this.id,
|
|
90
|
+
} satisfies ProtocolMediaResource;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -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(),
|
|
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
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
import { OrganizationId } from "./organizations";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Project ID schema, branded for type safety.
|
|
7
|
+
* @public
|
|
8
|
+
*/
|
|
9
|
+
export const ProjectId = z.string().nonempty().brand("ProjectId");
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Project ID type, branded for type safety.
|
|
13
|
+
* @public
|
|
14
|
+
*/
|
|
15
|
+
export type ProjectId = z.output<typeof ProjectId>;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Validates and brands a string as a ProjectId.
|
|
19
|
+
* @public
|
|
20
|
+
*/
|
|
21
|
+
export function brandProjectId(id: string): ProjectId {
|
|
22
|
+
return ProjectId.parse(id);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const ProjectMember = z.object({
|
|
26
|
+
id: z.string(),
|
|
27
|
+
createdAt: z.string(),
|
|
28
|
+
updatedAt: z.string(),
|
|
29
|
+
isCurrentUser: z.boolean(),
|
|
30
|
+
isRobot: z.boolean(),
|
|
31
|
+
roles: z.array(
|
|
32
|
+
z.object({
|
|
33
|
+
name: z.string(),
|
|
34
|
+
title: z.string(),
|
|
35
|
+
description: z.string(),
|
|
36
|
+
}),
|
|
37
|
+
),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @public
|
|
42
|
+
*/
|
|
43
|
+
export type ProjectMember = z.output<typeof ProjectMember>;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Project schema — validates and brands API responses
|
|
47
|
+
* from the `/projects/:id` endpoint.
|
|
48
|
+
* @public
|
|
49
|
+
*/
|
|
50
|
+
export const Project = z.object({
|
|
51
|
+
id: ProjectId,
|
|
52
|
+
displayName: z.string(),
|
|
53
|
+
studioHost: z.string().nullable(),
|
|
54
|
+
organizationId: OrganizationId,
|
|
55
|
+
metadata: z.object({
|
|
56
|
+
color: z.string().optional(),
|
|
57
|
+
externalStudioHost: z.string().optional(),
|
|
58
|
+
initialTemplate: z.string().optional(),
|
|
59
|
+
cliInitializedAt: z.string().optional(),
|
|
60
|
+
integration: z.literal(["manage", "cli"]),
|
|
61
|
+
}),
|
|
62
|
+
isBlocked: z.boolean(),
|
|
63
|
+
isDisabled: z.boolean(),
|
|
64
|
+
isDisabledByUser: z.boolean(),
|
|
65
|
+
activityFeedEnabled: z.boolean(),
|
|
66
|
+
createdAt: z.string(),
|
|
67
|
+
updatedAt: z.string(),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Represents a Sanity project with optional members and
|
|
72
|
+
* features arrays depending on the generic parameters.
|
|
73
|
+
* By default, neither members nor features are included.
|
|
74
|
+
* - `Project` — base fields only (default)
|
|
75
|
+
* - `Project<true>` — includes `members`
|
|
76
|
+
* - `Project<true, true>` — includes both
|
|
77
|
+
* @public
|
|
78
|
+
*/
|
|
79
|
+
export type Project<
|
|
80
|
+
IncludeMembers extends boolean = true,
|
|
81
|
+
IncludeFeatures extends boolean = true,
|
|
82
|
+
> = z.output<typeof Project> &
|
|
83
|
+
(IncludeMembers extends true ? { members: ProjectMember[] } : unknown) &
|
|
84
|
+
(IncludeFeatures extends true ? { features: string[] } : unknown);
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Validates and parses a raw API response into a branded
|
|
88
|
+
* Project. The options control which schema is used —
|
|
89
|
+
* matching what the API returns based on query params.
|
|
90
|
+
* @public
|
|
91
|
+
*/
|
|
92
|
+
export function parseProject<
|
|
93
|
+
IncludeMembers extends boolean = true,
|
|
94
|
+
IncludeFeatures extends boolean = true,
|
|
95
|
+
>(
|
|
96
|
+
data: unknown,
|
|
97
|
+
options?: {
|
|
98
|
+
includeMembers?: IncludeMembers;
|
|
99
|
+
includeFeatures?: IncludeFeatures;
|
|
100
|
+
},
|
|
101
|
+
): Project<IncludeMembers, IncludeFeatures> {
|
|
102
|
+
const includeMembers = options?.includeMembers ?? true;
|
|
103
|
+
const includeFeatures = options?.includeFeatures ?? true;
|
|
104
|
+
|
|
105
|
+
const extensions = {
|
|
106
|
+
...(includeMembers && { members: z.array(ProjectMember) }),
|
|
107
|
+
...(includeFeatures && { features: z.array(z.string()) }),
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const schema =
|
|
111
|
+
Object.keys(extensions).length > 0 ? Project.extend(extensions) : Project;
|
|
112
|
+
|
|
113
|
+
return schema.parse(data) as Project<IncludeMembers, IncludeFeatures>;
|
|
114
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Joins multiple path segments into a single path string.
|
|
3
|
+
* Handles null, undefined, and URL objects gracefully.
|
|
4
|
+
*
|
|
5
|
+
* @public
|
|
6
|
+
* @param paths - An array of path segments to join.
|
|
7
|
+
* @returns A single joined path string.
|
|
8
|
+
*/
|
|
9
|
+
export const joinUrlPaths = (
|
|
10
|
+
...paths: Array<string | URL | null | undefined>
|
|
11
|
+
): string => {
|
|
12
|
+
let nextPath = null;
|
|
13
|
+
|
|
14
|
+
const safeJoinSegments = (
|
|
15
|
+
segment1: string | null | undefined,
|
|
16
|
+
segment2: string | null | undefined,
|
|
17
|
+
): string => {
|
|
18
|
+
if (!segment1) {
|
|
19
|
+
if (!segment2) {
|
|
20
|
+
return "";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return segment2;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!segment2) {
|
|
27
|
+
return segment1;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (segment1.endsWith("/") && segment2.startsWith("/")) {
|
|
31
|
+
return segment1 + segment2.slice(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (segment1.endsWith("/") || segment2.startsWith("/")) {
|
|
35
|
+
return segment1 + segment2;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return `${segment1}/${segment2}`;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const validPaths = paths.filter(
|
|
42
|
+
(path) => path !== null && path !== undefined,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
for (const path of validPaths) {
|
|
46
|
+
nextPath = safeJoinSegments(
|
|
47
|
+
nextPath,
|
|
48
|
+
path instanceof URL ? path.pathname : path,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return nextPath ?? "";
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Returns a normalized path by ensuring it starts with a single leading slash
|
|
57
|
+
* and does not end with a trailing slash (unless it's the root path).
|
|
58
|
+
*/
|
|
59
|
+
export function normalizePath(pathname: string): string {
|
|
60
|
+
if (!pathname.startsWith("/")) {
|
|
61
|
+
pathname = `/${pathname}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (pathname !== "/" && pathname.endsWith("/")) {
|
|
65
|
+
pathname = pathname.slice(0, -1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return pathname;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The pathname, search, and hash values of a URL.
|
|
73
|
+
*/
|
|
74
|
+
interface Path {
|
|
75
|
+
/**
|
|
76
|
+
* A URL pathname, beginning with a /.
|
|
77
|
+
*/
|
|
78
|
+
pathname: string;
|
|
79
|
+
/**
|
|
80
|
+
* A URL search string, beginning with a ?.
|
|
81
|
+
*/
|
|
82
|
+
search: string;
|
|
83
|
+
/**
|
|
84
|
+
* A URL fragment identifier, beginning with a #.
|
|
85
|
+
*/
|
|
86
|
+
hash: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Parses a string URL path into its separate pathname, search, and hash components.
|
|
91
|
+
*/
|
|
92
|
+
export function parsePath(path: string): Partial<Path> {
|
|
93
|
+
let parsedPath: Partial<Path> = {};
|
|
94
|
+
|
|
95
|
+
if (path) {
|
|
96
|
+
let hashIndex = path.indexOf("#");
|
|
97
|
+
if (hashIndex >= 0) {
|
|
98
|
+
parsedPath.hash = path.substring(hashIndex);
|
|
99
|
+
path = path.substring(0, hashIndex);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let searchIndex = path.indexOf("?");
|
|
103
|
+
if (searchIndex >= 0) {
|
|
104
|
+
parsedPath.search = path.substring(searchIndex);
|
|
105
|
+
path = path.substring(0, searchIndex);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (path) {
|
|
109
|
+
parsedPath.pathname = path;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return parsedPath;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Creates a string URL path from the given pathname, search, and hash components.
|
|
118
|
+
*/
|
|
119
|
+
export function createPath({
|
|
120
|
+
pathname = "/",
|
|
121
|
+
search = "",
|
|
122
|
+
hash = "",
|
|
123
|
+
}: Partial<Path>) {
|
|
124
|
+
if (search && search !== "?")
|
|
125
|
+
pathname += search.charAt(0) === "?" ? search : `?${search}`;
|
|
126
|
+
if (hash && hash !== "#")
|
|
127
|
+
pathname += hash.charAt(0) === "#" ? hash : `#${hash}`;
|
|
128
|
+
return pathname;
|
|
129
|
+
}
|