@sanity/workbench 0.1.0-alpha.6 → 0.1.0-alpha.7
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/{log.js → _chunks-es/index.js} +1 -1
- package/dist/_chunks-es/index.js.map +1 -0
- package/dist/_internal.d.ts +13 -6
- package/dist/_internal.js +19 -3
- package/dist/_internal.js.map +1 -1
- package/dist/core.d.ts +906 -0
- package/dist/core.js +642 -0
- package/dist/core.js.map +1 -0
- package/package.json +10 -4
- package/src/_exports/core.ts +1 -0
- package/src/_internal/index.ts +2 -1
- package/src/_internal/render.test.ts +56 -1
- package/src/_internal/render.ts +51 -24
- package/src/core/__tests__/__fixtures__.ts +248 -0
- package/src/core/applications/application-list.test.ts +222 -0
- package/src/core/applications/application-list.ts +103 -0
- package/src/core/applications/application.ts +78 -0
- package/src/core/applications/local-application.test.ts +93 -0
- package/src/core/applications/local-application.ts +52 -0
- package/src/core/canvases.test.ts +38 -0
- package/src/core/canvases.ts +81 -0
- package/src/core/config.ts +34 -0
- package/src/core/index.ts +12 -0
- package/src/core/media-libraries.test.ts +38 -0
- package/src/core/media-libraries.ts +83 -0
- package/src/core/organizations.test.ts +134 -0
- package/src/core/organizations.ts +115 -0
- package/src/core/projects.test.ts +248 -0
- package/src/core/projects.ts +114 -0
- package/src/core/shared/urls.test.ts +182 -0
- package/src/core/shared/urls.ts +128 -0
- package/src/core/user-applications/core-app.test.ts +151 -0
- package/src/core/user-applications/core-app.ts +57 -0
- package/src/core/user-applications/studio.test.ts +928 -0
- package/src/core/user-applications/studio.ts +656 -0
- package/src/core/user-applications/user-application.test.ts +126 -0
- package/src/core/user-applications/user-application.ts +76 -0
- package/src/vite-env.d.ts +8 -0
- package/dist/log.d.ts +0 -48
- package/dist/log.js.map +0 -1
- package/src/_exports/log.ts +0 -1
- /package/src/{log → core/log}/index.test.ts +0 -0
- /package/src/{log → core/log}/index.ts +0 -0
|
@@ -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,182 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { createPath, joinUrlPaths, normalizePath, parsePath } from "./urls";
|
|
4
|
+
|
|
5
|
+
describe("joinUrlPaths", () => {
|
|
6
|
+
it("joins two simple segments", () => {
|
|
7
|
+
expect(joinUrlPaths("foo", "bar")).toBe("foo/bar");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("handles trailing slash on first segment", () => {
|
|
11
|
+
expect(joinUrlPaths("foo/", "bar")).toBe("foo/bar");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("handles leading slash on second segment", () => {
|
|
15
|
+
expect(joinUrlPaths("foo", "/bar")).toBe("foo/bar");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("collapses double slashes at the join", () => {
|
|
19
|
+
expect(joinUrlPaths("foo/", "/bar")).toBe("foo/bar");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("joins multiple segments", () => {
|
|
23
|
+
expect(joinUrlPaths("a", "b", "c")).toBe("a/b/c");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("skips null and undefined values", () => {
|
|
27
|
+
expect(joinUrlPaths("a", null, undefined, "b")).toBe("a/b");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("returns empty string for all nullish inputs", () => {
|
|
31
|
+
expect(joinUrlPaths(null, undefined)).toBe("");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("returns empty string with no arguments", () => {
|
|
35
|
+
expect(joinUrlPaths()).toBe("");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("returns single segment unchanged", () => {
|
|
39
|
+
expect(joinUrlPaths("foo")).toBe("foo");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("extracts pathname from URL objects", () => {
|
|
43
|
+
const url = new URL("https://example.com/api");
|
|
44
|
+
expect(joinUrlPaths(url, "users")).toBe("/api/users");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("handles URL object with trailing slash", () => {
|
|
48
|
+
const url = new URL("https://example.com/api/");
|
|
49
|
+
expect(joinUrlPaths(url, "users")).toBe("/api/users");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("joins two URL objects", () => {
|
|
53
|
+
const a = new URL("https://example.com/api");
|
|
54
|
+
const b = new URL("https://example.com/v2");
|
|
55
|
+
expect(joinUrlPaths(a, b)).toBe("/api/v2");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("normalizePath", () => {
|
|
60
|
+
it("adds leading slash when missing", () => {
|
|
61
|
+
expect(normalizePath("foo")).toBe("/foo");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("preserves existing leading slash", () => {
|
|
65
|
+
expect(normalizePath("/foo")).toBe("/foo");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("removes trailing slash", () => {
|
|
69
|
+
expect(normalizePath("/foo/")).toBe("/foo");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("preserves root path", () => {
|
|
73
|
+
expect(normalizePath("/")).toBe("/");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("handles bare segment with trailing slash", () => {
|
|
77
|
+
expect(normalizePath("foo/")).toBe("/foo");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("handles deeper paths", () => {
|
|
81
|
+
expect(normalizePath("/a/b/c/")).toBe("/a/b/c");
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("parsePath", () => {
|
|
86
|
+
it("parses pathname only", () => {
|
|
87
|
+
expect(parsePath("/foo")).toEqual({ pathname: "/foo" });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("parses pathname with search", () => {
|
|
91
|
+
expect(parsePath("/foo?bar=1")).toEqual({
|
|
92
|
+
pathname: "/foo",
|
|
93
|
+
search: "?bar=1",
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("parses pathname with hash", () => {
|
|
98
|
+
expect(parsePath("/foo#heading")).toEqual({
|
|
99
|
+
pathname: "/foo",
|
|
100
|
+
hash: "#heading",
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("parses pathname with search and hash", () => {
|
|
105
|
+
expect(parsePath("/foo?bar=1#heading")).toEqual({
|
|
106
|
+
pathname: "/foo",
|
|
107
|
+
search: "?bar=1",
|
|
108
|
+
hash: "#heading",
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("parses hash that appears before search in the string", () => {
|
|
113
|
+
expect(parsePath("/foo#heading?notSearch")).toEqual({
|
|
114
|
+
pathname: "/foo",
|
|
115
|
+
hash: "#heading?notSearch",
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("returns empty object for empty string", () => {
|
|
120
|
+
expect(parsePath("")).toEqual({});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("parses search-only path", () => {
|
|
124
|
+
expect(parsePath("?q=1")).toEqual({ search: "?q=1" });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("parses hash-only path", () => {
|
|
128
|
+
expect(parsePath("#top")).toEqual({ hash: "#top" });
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("createPath", () => {
|
|
133
|
+
it("returns pathname only", () => {
|
|
134
|
+
expect(createPath({ pathname: "/foo" })).toBe("/foo");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("appends search string", () => {
|
|
138
|
+
expect(createPath({ pathname: "/foo", search: "?bar=1" })).toBe(
|
|
139
|
+
"/foo?bar=1",
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("appends hash string", () => {
|
|
144
|
+
expect(createPath({ pathname: "/foo", hash: "#heading" })).toBe(
|
|
145
|
+
"/foo#heading",
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("appends both search and hash", () => {
|
|
150
|
+
expect(
|
|
151
|
+
createPath({
|
|
152
|
+
pathname: "/foo",
|
|
153
|
+
search: "?bar=1",
|
|
154
|
+
hash: "#heading",
|
|
155
|
+
}),
|
|
156
|
+
).toBe("/foo?bar=1#heading");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("adds ? prefix when search is missing it", () => {
|
|
160
|
+
expect(createPath({ pathname: "/foo", search: "bar=1" })).toBe(
|
|
161
|
+
"/foo?bar=1",
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("adds # prefix when hash is missing it", () => {
|
|
166
|
+
expect(createPath({ pathname: "/foo", hash: "heading" })).toBe(
|
|
167
|
+
"/foo#heading",
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("defaults pathname to /", () => {
|
|
172
|
+
expect(createPath({})).toBe("/");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("ignores bare ? search", () => {
|
|
176
|
+
expect(createPath({ pathname: "/foo", search: "?" })).toBe("/foo");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("ignores bare # hash", () => {
|
|
180
|
+
expect(createPath({ pathname: "/foo", hash: "#" })).toBe("/foo");
|
|
181
|
+
});
|
|
182
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Joins multiple path segments into a single path string.
|
|
3
|
+
* Handles null, undefined, and URL objects gracefully.
|
|
4
|
+
*
|
|
5
|
+
* @param paths - An array of path segments to join.
|
|
6
|
+
* @returns A single joined path string.
|
|
7
|
+
*/
|
|
8
|
+
export const joinUrlPaths = (
|
|
9
|
+
...paths: Array<string | URL | null | undefined>
|
|
10
|
+
): string => {
|
|
11
|
+
let nextPath = null;
|
|
12
|
+
|
|
13
|
+
const safeJoinSegments = (
|
|
14
|
+
segment1: string | null | undefined,
|
|
15
|
+
segment2: string | null | undefined,
|
|
16
|
+
): string => {
|
|
17
|
+
if (!segment1) {
|
|
18
|
+
if (!segment2) {
|
|
19
|
+
return "";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return segment2;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!segment2) {
|
|
26
|
+
return segment1;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (segment1.endsWith("/") && segment2.startsWith("/")) {
|
|
30
|
+
return segment1 + segment2.slice(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (segment1.endsWith("/") || segment2.startsWith("/")) {
|
|
34
|
+
return segment1 + segment2;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return `${segment1}/${segment2}`;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const validPaths = paths.filter(
|
|
41
|
+
(path) => path !== null && path !== undefined,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
for (const path of validPaths) {
|
|
45
|
+
nextPath = safeJoinSegments(
|
|
46
|
+
nextPath,
|
|
47
|
+
path instanceof URL ? path.pathname : path,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return nextPath ?? "";
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Returns a normalized path by ensuring it starts with a single leading slash
|
|
56
|
+
* and does not end with a trailing slash (unless it's the root path).
|
|
57
|
+
*/
|
|
58
|
+
export function normalizePath(pathname: string): string {
|
|
59
|
+
if (!pathname.startsWith("/")) {
|
|
60
|
+
pathname = `/${pathname}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (pathname !== "/" && pathname.endsWith("/")) {
|
|
64
|
+
pathname = pathname.slice(0, -1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return pathname;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The pathname, search, and hash values of a URL.
|
|
72
|
+
*/
|
|
73
|
+
interface Path {
|
|
74
|
+
/**
|
|
75
|
+
* A URL pathname, beginning with a /.
|
|
76
|
+
*/
|
|
77
|
+
pathname: string;
|
|
78
|
+
/**
|
|
79
|
+
* A URL search string, beginning with a ?.
|
|
80
|
+
*/
|
|
81
|
+
search: string;
|
|
82
|
+
/**
|
|
83
|
+
* A URL fragment identifier, beginning with a #.
|
|
84
|
+
*/
|
|
85
|
+
hash: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Parses a string URL path into its separate pathname, search, and hash components.
|
|
90
|
+
*/
|
|
91
|
+
export function parsePath(path: string): Partial<Path> {
|
|
92
|
+
let parsedPath: Partial<Path> = {};
|
|
93
|
+
|
|
94
|
+
if (path) {
|
|
95
|
+
let hashIndex = path.indexOf("#");
|
|
96
|
+
if (hashIndex >= 0) {
|
|
97
|
+
parsedPath.hash = path.substring(hashIndex);
|
|
98
|
+
path = path.substring(0, hashIndex);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let searchIndex = path.indexOf("?");
|
|
102
|
+
if (searchIndex >= 0) {
|
|
103
|
+
parsedPath.search = path.substring(searchIndex);
|
|
104
|
+
path = path.substring(0, searchIndex);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (path) {
|
|
108
|
+
parsedPath.pathname = path;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return parsedPath;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Creates a string URL path from the given pathname, search, and hash components.
|
|
117
|
+
*/
|
|
118
|
+
export function createPath({
|
|
119
|
+
pathname = "/",
|
|
120
|
+
search = "",
|
|
121
|
+
hash = "",
|
|
122
|
+
}: Partial<Path>) {
|
|
123
|
+
if (search && search !== "?")
|
|
124
|
+
pathname += search.charAt(0) === "?" ? search : `?${search}`;
|
|
125
|
+
if (hash && hash !== "#")
|
|
126
|
+
pathname += hash.charAt(0) === "#" ? hash : `#${hash}`;
|
|
127
|
+
return pathname;
|
|
128
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import type { ApplicationResource as ProtocolApplicationResource } from "@sanity/message-protocol";
|
|
2
|
+
import { afterEach, describe, expect, expectTypeOf, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { APPLICATION_DATA } from "../__tests__/__fixtures__";
|
|
5
|
+
import { CoreAppApplication } from "./core-app";
|
|
6
|
+
|
|
7
|
+
const setup = () => new CoreAppApplication(APPLICATION_DATA);
|
|
8
|
+
|
|
9
|
+
describe("CoreAppApplication", () => {
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
vi.unstubAllEnvs();
|
|
12
|
+
});
|
|
13
|
+
it("should have a type of coreApp", () => {
|
|
14
|
+
const application = setup();
|
|
15
|
+
expect(application.type).toBe("coreApp");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("should have a href of <application>/<id>", () => {
|
|
19
|
+
const application = setup();
|
|
20
|
+
expect(application.href).toBe("/application/v27rvqtlp3lmdvcln6ey3lro");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("should have a name of the application title", () => {
|
|
24
|
+
const application = setup();
|
|
25
|
+
expect(application.title).toBe("sdk-movie-list");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("should have a updatedAt property", () => {
|
|
29
|
+
const application = setup();
|
|
30
|
+
expect(application.updatedAt).toBe("2025-03-27T19:00:32.792Z");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should have an activeDeployment property", () => {
|
|
34
|
+
const application = setup();
|
|
35
|
+
expect(application.activeDeployment).toMatchInlineSnapshot(`
|
|
36
|
+
{
|
|
37
|
+
"createdAt": "2025-03-27T19:07:21.038Z",
|
|
38
|
+
"deployedAt": "2025-03-27T19:07:21.082Z",
|
|
39
|
+
"deployedBy": "gwXueEBci",
|
|
40
|
+
"id": "dv3kz3fsl4aqha3parc8k391",
|
|
41
|
+
"isActiveDeployment": true,
|
|
42
|
+
"isAutoUpdating": false,
|
|
43
|
+
"manifest": null,
|
|
44
|
+
"size": 528292,
|
|
45
|
+
"updatedAt": "2025-03-27T19:07:21.082Z",
|
|
46
|
+
"userApplicationId": "v27rvqtlp3lmdvcln6ey3lro",
|
|
47
|
+
"version": "3.81.0",
|
|
48
|
+
}
|
|
49
|
+
`);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should implement the toProtocolResource method", () => {
|
|
53
|
+
vi.stubEnv("VITE_SANITY_ENV", "staging");
|
|
54
|
+
vi.stubEnv("VITE_SANITY_DOMAIN", "sanity.work");
|
|
55
|
+
const application = setup();
|
|
56
|
+
expect(application.toProtocolResource()).toMatchInlineSnapshot(`
|
|
57
|
+
{
|
|
58
|
+
"activeDeployment": {
|
|
59
|
+
"createdAt": "2025-03-27T19:07:21.038Z",
|
|
60
|
+
"deployedAt": "2025-03-27T19:07:21.082Z",
|
|
61
|
+
"deployedBy": "gwXueEBci",
|
|
62
|
+
"id": "dv3kz3fsl4aqha3parc8k391",
|
|
63
|
+
"isActiveDeployment": true,
|
|
64
|
+
"isAutoUpdating": false,
|
|
65
|
+
"manifest": null,
|
|
66
|
+
"size": 528292,
|
|
67
|
+
"updatedAt": "2025-03-27T19:07:21.082Z",
|
|
68
|
+
"userApplicationId": "v27rvqtlp3lmdvcln6ey3lro",
|
|
69
|
+
"version": "3.81.0",
|
|
70
|
+
},
|
|
71
|
+
"appHost": "x7apsmr6fxvc",
|
|
72
|
+
"createdAt": "2025-03-27T19:00:32.792Z",
|
|
73
|
+
"dashboardStatus": "default",
|
|
74
|
+
"id": "v27rvqtlp3lmdvcln6ey3lro",
|
|
75
|
+
"organizationId": "oSyH1iET5",
|
|
76
|
+
"projectId": null,
|
|
77
|
+
"title": "sdk-movie-list",
|
|
78
|
+
"type": "application",
|
|
79
|
+
"updatedAt": "2025-03-27T19:00:32.792Z",
|
|
80
|
+
"url": "https://x7apsmr6fxvc.studio.sanity.work/",
|
|
81
|
+
"urlType": "internal",
|
|
82
|
+
}
|
|
83
|
+
`);
|
|
84
|
+
|
|
85
|
+
expectTypeOf(
|
|
86
|
+
application.toProtocolResource(),
|
|
87
|
+
).toEqualTypeOf<ProtocolApplicationResource>();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("should return undefined for subtitle", () => {
|
|
91
|
+
const application = setup();
|
|
92
|
+
expect(application.subtitle).toBeUndefined();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("get", () => {
|
|
96
|
+
it("should throw when accessing a non-existent attribute", () => {
|
|
97
|
+
const application = setup();
|
|
98
|
+
expect(() =>
|
|
99
|
+
// @ts-expect-error - testing invalid attribute
|
|
100
|
+
application.get("nonExistent"),
|
|
101
|
+
).toThrow(
|
|
102
|
+
"Attribute nonExistent does not exist on application v27rvqtlp3lmdvcln6ey3lro",
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("manifest functionality", () => {
|
|
108
|
+
it("should use manifest from application when present", () => {
|
|
109
|
+
const applicationWithManifest = {
|
|
110
|
+
...APPLICATION_DATA,
|
|
111
|
+
activeDeployment: {
|
|
112
|
+
...APPLICATION_DATA.activeDeployment,
|
|
113
|
+
manifest: {
|
|
114
|
+
version: "1.0.0",
|
|
115
|
+
icon: "<svg>test-icon</svg>",
|
|
116
|
+
title: "Manifest Title",
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
const application = new CoreAppApplication(applicationWithManifest);
|
|
121
|
+
expect(application.title).toBe("Manifest Title");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("should use manifest from activeDeployment when application manifest is not present", () => {
|
|
125
|
+
const applicationWithDeploymentManifest = {
|
|
126
|
+
...APPLICATION_DATA,
|
|
127
|
+
activeDeployment: {
|
|
128
|
+
...APPLICATION_DATA.activeDeployment,
|
|
129
|
+
manifest: {
|
|
130
|
+
version: "1.0.0",
|
|
131
|
+
icon: "<svg>deployment-icon</svg>",
|
|
132
|
+
title: "Deployment Title",
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
const application = new CoreAppApplication(
|
|
137
|
+
applicationWithDeploymentManifest,
|
|
138
|
+
);
|
|
139
|
+
expect(application.title).toBe("Deployment Title");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("should allow you to get attributes from the application", () => {
|
|
143
|
+
const application = setup();
|
|
144
|
+
|
|
145
|
+
expect(application.get("title")).toBe("sdk-movie-list");
|
|
146
|
+
expect(application.get("urlType")).toBe("internal");
|
|
147
|
+
expect(application.get("appHost")).toBe("x7apsmr6fxvc");
|
|
148
|
+
expectTypeOf(application.get("appHost")).toEqualTypeOf<string>();
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ApplicationResource as ProtocolApplicationResource,
|
|
3
|
+
CoreApplication,
|
|
4
|
+
} from "@sanity/message-protocol";
|
|
5
|
+
|
|
6
|
+
import { UserApplication } from "./user-application";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
export class CoreAppApplication extends UserApplication<
|
|
12
|
+
CoreApplication,
|
|
13
|
+
"coreApp",
|
|
14
|
+
ProtocolApplicationResource
|
|
15
|
+
> {
|
|
16
|
+
readonly activeDeployment: CoreApplication["activeDeployment"];
|
|
17
|
+
|
|
18
|
+
constructor(application: CoreApplication) {
|
|
19
|
+
super(application, "coreApp");
|
|
20
|
+
|
|
21
|
+
this.activeDeployment = application.activeDeployment;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
get href() {
|
|
25
|
+
return `/application/${this.application.id}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get title() {
|
|
29
|
+
return this.activeDeployment?.manifest?.title ?? this.application.title;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get subtitle() {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get updatedAt() {
|
|
37
|
+
return this.application.updatedAt;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get<TKey extends keyof CoreApplication>(attr: TKey): CoreApplication[TKey] {
|
|
41
|
+
if (!(attr in this.application)) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`Attribute ${attr.toString()} does not exist on application ${this.application.id}`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return this.application[attr];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
toProtocolResource(): ProtocolApplicationResource {
|
|
51
|
+
return {
|
|
52
|
+
...this.application,
|
|
53
|
+
type: "application",
|
|
54
|
+
url: this.url.toString(),
|
|
55
|
+
} satisfies ProtocolApplicationResource;
|
|
56
|
+
}
|
|
57
|
+
}
|