@sanity/workbench 0.1.0-alpha.6 → 0.1.0-alpha.8
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} +7 -2
- package/dist/_chunks-es/index.js.map +1 -0
- package/dist/_internal.d.ts +13 -6
- package/dist/_internal.js +20 -9
- package/dist/_internal.js.map +1 -1
- package/dist/core.d.ts +1464 -0
- package/dist/core.js +744 -0
- package/dist/core.js.map +1 -0
- package/package.json +12 -4
- package/src/_exports/core.ts +1 -0
- package/src/_internal/index.ts +2 -1
- package/src/_internal/render.test.ts +91 -4
- package/src/_internal/render.ts +53 -33
- package/src/core/__tests__/__fixtures__.ts +245 -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 +56 -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/{log → core/log}/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 +236 -0
- package/src/core/user-applications/core-app.ts +113 -0
- package/src/core/user-applications/studios/index.ts +3 -0
- package/src/core/user-applications/studios/schemas.test.ts +113 -0
- package/src/core/user-applications/studios/schemas.ts +106 -0
- package/src/core/user-applications/studios/studio.test.ts +997 -0
- package/src/core/user-applications/studios/studio.ts +498 -0
- package/src/core/user-applications/studios/workspace.ts +143 -0
- package/src/core/user-applications/user-application.test.ts +125 -0
- package/src/core/user-applications/user-application.ts +107 -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
|
@@ -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,236 @@
|
|
|
1
|
+
// eslint-disable-next-line no-restricted-imports
|
|
2
|
+
import type { ApplicationResource as ProtocolApplicationResource } from "@sanity/message-protocol";
|
|
3
|
+
import { afterEach, describe, expect, expectTypeOf, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { APPLICATION_DATA } from "../__tests__/__fixtures__";
|
|
6
|
+
import {
|
|
7
|
+
CoreAppApplication,
|
|
8
|
+
parseCoreApplication,
|
|
9
|
+
type CoreAppUserApplication,
|
|
10
|
+
} from "./core-app";
|
|
11
|
+
import type { UserApplicationId } from "./user-application";
|
|
12
|
+
|
|
13
|
+
const setup = () => new CoreAppApplication(APPLICATION_DATA);
|
|
14
|
+
|
|
15
|
+
describe("CoreAppApplication", () => {
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
vi.unstubAllEnvs();
|
|
18
|
+
});
|
|
19
|
+
it("should have a type of coreApp", () => {
|
|
20
|
+
const application = setup();
|
|
21
|
+
expect(application.type).toBe("coreApp");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("should have a href of <application>/<id>", () => {
|
|
25
|
+
const application = setup();
|
|
26
|
+
expect(application.href).toBe("/application/v27rvqtlp3lmdvcln6ey3lro");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should have a name of the application title", () => {
|
|
30
|
+
const application = setup();
|
|
31
|
+
expect(application.title).toBe("sdk-movie-list");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("should have a updatedAt property", () => {
|
|
35
|
+
const application = setup();
|
|
36
|
+
expect(application.updatedAt).toBe("2025-03-27T19:00:32.792Z");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("should have an activeDeployment property", () => {
|
|
40
|
+
const application = setup();
|
|
41
|
+
expect(application.activeDeployment).toMatchInlineSnapshot(`
|
|
42
|
+
{
|
|
43
|
+
"createdAt": "2025-03-27T19:07:21.038Z",
|
|
44
|
+
"deployedAt": "2025-03-27T19:07:21.082Z",
|
|
45
|
+
"deployedBy": "gwXueEBci",
|
|
46
|
+
"id": "dv3kz3fsl4aqha3parc8k391",
|
|
47
|
+
"isActiveDeployment": true,
|
|
48
|
+
"isAutoUpdating": false,
|
|
49
|
+
"manifest": null,
|
|
50
|
+
"size": 528292,
|
|
51
|
+
"updatedAt": "2025-03-27T19:07:21.082Z",
|
|
52
|
+
"userApplicationId": "v27rvqtlp3lmdvcln6ey3lro",
|
|
53
|
+
"version": "3.81.0",
|
|
54
|
+
}
|
|
55
|
+
`);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should implement the toProtocolResource method", () => {
|
|
59
|
+
vi.stubEnv("VITE_SANITY_ENV", "staging");
|
|
60
|
+
vi.stubEnv("VITE_SANITY_DOMAIN", "sanity.work");
|
|
61
|
+
const application = setup();
|
|
62
|
+
expect(application.toProtocolResource()).toMatchInlineSnapshot(`
|
|
63
|
+
{
|
|
64
|
+
"activeDeployment": {
|
|
65
|
+
"createdAt": "2025-03-27T19:07:21.038Z",
|
|
66
|
+
"deployedAt": "2025-03-27T19:07:21.082Z",
|
|
67
|
+
"deployedBy": "gwXueEBci",
|
|
68
|
+
"id": "dv3kz3fsl4aqha3parc8k391",
|
|
69
|
+
"isActiveDeployment": true,
|
|
70
|
+
"isAutoUpdating": false,
|
|
71
|
+
"manifest": null,
|
|
72
|
+
"size": 528292,
|
|
73
|
+
"updatedAt": "2025-03-27T19:07:21.082Z",
|
|
74
|
+
"userApplicationId": "v27rvqtlp3lmdvcln6ey3lro",
|
|
75
|
+
"version": "3.81.0",
|
|
76
|
+
},
|
|
77
|
+
"appHost": "x7apsmr6fxvc",
|
|
78
|
+
"createdAt": "2025-03-27T19:00:32.792Z",
|
|
79
|
+
"dashboardStatus": "default",
|
|
80
|
+
"id": "v27rvqtlp3lmdvcln6ey3lro",
|
|
81
|
+
"organizationId": "oSyH1iET5",
|
|
82
|
+
"title": "sdk-movie-list",
|
|
83
|
+
"type": "application",
|
|
84
|
+
"updatedAt": "2025-03-27T19:00:32.792Z",
|
|
85
|
+
"url": "https://x7apsmr6fxvc.studio.sanity.work/",
|
|
86
|
+
"urlType": "internal",
|
|
87
|
+
}
|
|
88
|
+
`);
|
|
89
|
+
|
|
90
|
+
expectTypeOf(
|
|
91
|
+
application.toProtocolResource(),
|
|
92
|
+
).toEqualTypeOf<ProtocolApplicationResource>();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("should return undefined for subtitle", () => {
|
|
96
|
+
const application = setup();
|
|
97
|
+
expect(application.subtitle).toBeUndefined();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("get", () => {
|
|
101
|
+
it("should throw when accessing a non-existent attribute", () => {
|
|
102
|
+
const application = setup();
|
|
103
|
+
expect(() =>
|
|
104
|
+
// @ts-expect-error - testing invalid attribute
|
|
105
|
+
application.get("nonExistent"),
|
|
106
|
+
).toThrow(
|
|
107
|
+
"Attribute nonExistent does not exist on application v27rvqtlp3lmdvcln6ey3lro",
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("manifest functionality", () => {
|
|
113
|
+
it("should use manifest from application when present", () => {
|
|
114
|
+
const application = new CoreAppApplication(
|
|
115
|
+
parseCoreApplication({
|
|
116
|
+
...APPLICATION_DATA,
|
|
117
|
+
activeDeployment: {
|
|
118
|
+
...APPLICATION_DATA.activeDeployment!,
|
|
119
|
+
manifest: {
|
|
120
|
+
version: "1.0.0",
|
|
121
|
+
icon: "<svg>test-icon</svg>",
|
|
122
|
+
title: "Manifest Title",
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
}),
|
|
126
|
+
);
|
|
127
|
+
expect(application.title).toBe("Manifest Title");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("should use manifest from activeDeployment when application manifest is not present", () => {
|
|
131
|
+
const application = new CoreAppApplication(
|
|
132
|
+
parseCoreApplication({
|
|
133
|
+
...APPLICATION_DATA,
|
|
134
|
+
activeDeployment: {
|
|
135
|
+
...APPLICATION_DATA.activeDeployment!,
|
|
136
|
+
manifest: {
|
|
137
|
+
version: "1.0.0",
|
|
138
|
+
icon: "<svg>deployment-icon</svg>",
|
|
139
|
+
title: "Deployment Title",
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
}),
|
|
143
|
+
);
|
|
144
|
+
expect(application.title).toBe("Deployment Title");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("should allow you to get attributes from the application", () => {
|
|
148
|
+
const application = setup();
|
|
149
|
+
|
|
150
|
+
expect(application.get("title")).toBe("sdk-movie-list");
|
|
151
|
+
expect(application.get("urlType")).toBe("internal");
|
|
152
|
+
expect(application.get("appHost")).toBe("x7apsmr6fxvc");
|
|
153
|
+
expectTypeOf(application.get("appHost")).toEqualTypeOf<string>();
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const VALID_INTERNAL_CORE_APP = {
|
|
159
|
+
id: "app-001",
|
|
160
|
+
appHost: "my-app-host",
|
|
161
|
+
createdAt: "2025-01-01T00:00:00Z",
|
|
162
|
+
updatedAt: "2025-01-01T00:00:00Z",
|
|
163
|
+
dashboardStatus: "default",
|
|
164
|
+
title: "My App",
|
|
165
|
+
organizationId: "org-001",
|
|
166
|
+
type: "coreApp",
|
|
167
|
+
urlType: "internal",
|
|
168
|
+
activeDeployment: {
|
|
169
|
+
id: "deploy-001",
|
|
170
|
+
version: "1.0.0",
|
|
171
|
+
isActiveDeployment: true,
|
|
172
|
+
userApplicationId: "app-001",
|
|
173
|
+
isAutoUpdating: false,
|
|
174
|
+
manifest: null,
|
|
175
|
+
size: 500,
|
|
176
|
+
deployedAt: "2025-01-01T00:00:00Z",
|
|
177
|
+
deployedBy: "user-001",
|
|
178
|
+
createdAt: "2025-01-01T00:00:00Z",
|
|
179
|
+
updatedAt: "2025-01-01T00:00:00Z",
|
|
180
|
+
},
|
|
181
|
+
} as const;
|
|
182
|
+
|
|
183
|
+
const VALID_EXTERNAL_CORE_APP = {
|
|
184
|
+
id: "app-002",
|
|
185
|
+
appHost: "https://my-app.example.com",
|
|
186
|
+
createdAt: "2025-01-01T00:00:00Z",
|
|
187
|
+
updatedAt: "2025-01-01T00:00:00Z",
|
|
188
|
+
dashboardStatus: "default",
|
|
189
|
+
title: "My External App",
|
|
190
|
+
organizationId: "org-001",
|
|
191
|
+
type: "coreApp",
|
|
192
|
+
urlType: "external",
|
|
193
|
+
activeDeployment: null,
|
|
194
|
+
} as const;
|
|
195
|
+
|
|
196
|
+
describe("parseCoreApplication", () => {
|
|
197
|
+
it("parses a valid internal core application", () => {
|
|
198
|
+
const app = parseCoreApplication(VALID_INTERNAL_CORE_APP);
|
|
199
|
+
expect(app.urlType).toBe("internal");
|
|
200
|
+
expect(app.title).toBe("My App");
|
|
201
|
+
expectTypeOf(app).toEqualTypeOf<CoreAppUserApplication>();
|
|
202
|
+
expectTypeOf(app.id).toEqualTypeOf<UserApplicationId>();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("parses a valid external core application", () => {
|
|
206
|
+
const app = parseCoreApplication(VALID_EXTERNAL_CORE_APP);
|
|
207
|
+
expect(app.urlType).toBe("external");
|
|
208
|
+
expect(app.activeDeployment).toBeNull();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("rejects missing required fields", () => {
|
|
212
|
+
expect(() => parseCoreApplication({ id: "app-001" })).toThrow();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("rejects an empty id", () => {
|
|
216
|
+
expect(() =>
|
|
217
|
+
parseCoreApplication({ ...VALID_INTERNAL_CORE_APP, id: "" }),
|
|
218
|
+
).toThrow();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("parses core app with deployment manifest", () => {
|
|
222
|
+
const app = parseCoreApplication({
|
|
223
|
+
...VALID_INTERNAL_CORE_APP,
|
|
224
|
+
activeDeployment: {
|
|
225
|
+
...VALID_INTERNAL_CORE_APP.activeDeployment,
|
|
226
|
+
manifest: {
|
|
227
|
+
version: "1.0.0",
|
|
228
|
+
title: "Manifest Title",
|
|
229
|
+
icon: "<svg></svg>",
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
expect(app.activeDeployment?.manifest?.title).toBe("Manifest Title");
|
|
235
|
+
});
|
|
236
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// eslint-disable-next-line no-restricted-imports
|
|
2
|
+
import type { ApplicationResource as ProtocolApplicationResource } from "@sanity/message-protocol";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
import { OrganizationId } from "../organizations";
|
|
6
|
+
import {
|
|
7
|
+
UserApplication,
|
|
8
|
+
UserApplicationBase,
|
|
9
|
+
ActiveDeployment,
|
|
10
|
+
} from "./user-application";
|
|
11
|
+
|
|
12
|
+
const CoreAppUserApplicationManifest = z.object({
|
|
13
|
+
version: z.string(),
|
|
14
|
+
icon: z.string().optional(),
|
|
15
|
+
title: z.string().optional(),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const CoreAppUserApplicationBase = UserApplicationBase.extend({
|
|
19
|
+
title: z.string(),
|
|
20
|
+
organizationId: OrganizationId,
|
|
21
|
+
type: z.literal("coreApp"),
|
|
22
|
+
manifest: CoreAppUserApplicationManifest.nullable().optional(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const InternalCoreAppUserApplication = CoreAppUserApplicationBase.extend({
|
|
26
|
+
urlType: z.literal("internal"),
|
|
27
|
+
activeDeployment: ActiveDeployment.extend({
|
|
28
|
+
manifest: CoreAppUserApplicationManifest.nullable().optional(),
|
|
29
|
+
}),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const ExternalCoreAppUserApplication = CoreAppUserApplicationBase.extend({
|
|
33
|
+
urlType: z.literal("external"),
|
|
34
|
+
activeDeployment: z.null(),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Core application schema — validates and brands API
|
|
39
|
+
* responses from the `/user-applications?appType=coreApp`
|
|
40
|
+
* endpoint. Uses a discriminated union on `urlType`.
|
|
41
|
+
* @public
|
|
42
|
+
*/
|
|
43
|
+
const CoreAppUserApplication = z.discriminatedUnion("urlType", [
|
|
44
|
+
InternalCoreAppUserApplication,
|
|
45
|
+
ExternalCoreAppUserApplication,
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @public
|
|
50
|
+
*/
|
|
51
|
+
export type CoreAppUserApplication = z.output<typeof CoreAppUserApplication>;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Validates and parses a raw API response into a branded
|
|
55
|
+
* CoreAppUserApplicationData.
|
|
56
|
+
* @public
|
|
57
|
+
*/
|
|
58
|
+
export function parseCoreApplication(data: unknown): CoreAppUserApplication {
|
|
59
|
+
return CoreAppUserApplication.parse(data);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @internal
|
|
64
|
+
*/
|
|
65
|
+
export class CoreAppApplication extends UserApplication<
|
|
66
|
+
CoreAppUserApplication,
|
|
67
|
+
"coreApp",
|
|
68
|
+
ProtocolApplicationResource
|
|
69
|
+
> {
|
|
70
|
+
readonly activeDeployment: CoreAppUserApplication["activeDeployment"];
|
|
71
|
+
|
|
72
|
+
constructor(application: CoreAppUserApplication) {
|
|
73
|
+
super(application, "coreApp");
|
|
74
|
+
|
|
75
|
+
this.activeDeployment = application.activeDeployment;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
get href() {
|
|
79
|
+
return `/application/${this.application.id}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
get title() {
|
|
83
|
+
return this.activeDeployment?.manifest?.title ?? this.application.title;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
get subtitle() {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
get updatedAt() {
|
|
91
|
+
return this.application.updatedAt;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
get<TKey extends keyof CoreAppUserApplication>(
|
|
95
|
+
attr: TKey,
|
|
96
|
+
): CoreAppUserApplication[TKey] {
|
|
97
|
+
if (!(attr in this.application)) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`Attribute ${attr.toString()} does not exist on application ${this.application.id}`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return this.application[attr];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
toProtocolResource(): ProtocolApplicationResource {
|
|
107
|
+
return {
|
|
108
|
+
...this.application,
|
|
109
|
+
type: "application",
|
|
110
|
+
url: this.url.toString(),
|
|
111
|
+
} as ProtocolApplicationResource;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { describe, expect, expectTypeOf, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import type { UserApplicationId } from "../user-application";
|
|
4
|
+
import {
|
|
5
|
+
parseStudioUserApplication,
|
|
6
|
+
type StudioUserApplication,
|
|
7
|
+
} from "./schemas";
|
|
8
|
+
|
|
9
|
+
const VALID_INTERNAL_STUDIO = {
|
|
10
|
+
id: "studio-001",
|
|
11
|
+
appHost: "my-studio",
|
|
12
|
+
createdAt: "2025-01-01T00:00:00Z",
|
|
13
|
+
updatedAt: "2025-01-01T00:00:00Z",
|
|
14
|
+
dashboardStatus: "default",
|
|
15
|
+
title: null,
|
|
16
|
+
projectId: "proj-001",
|
|
17
|
+
type: "studio",
|
|
18
|
+
urlType: "internal",
|
|
19
|
+
manifest: null,
|
|
20
|
+
manifestData: null,
|
|
21
|
+
autoUpdatingVersion: "latest",
|
|
22
|
+
activeDeployment: {
|
|
23
|
+
id: "deploy-001",
|
|
24
|
+
version: "4.0.0",
|
|
25
|
+
isActiveDeployment: true,
|
|
26
|
+
userApplicationId: "studio-001",
|
|
27
|
+
isAutoUpdating: true,
|
|
28
|
+
manifest: null,
|
|
29
|
+
size: 1000,
|
|
30
|
+
deployedAt: "2025-01-01T00:00:00Z",
|
|
31
|
+
deployedBy: "user-001",
|
|
32
|
+
createdAt: "2025-01-01T00:00:00Z",
|
|
33
|
+
updatedAt: "2025-01-01T00:00:00Z",
|
|
34
|
+
},
|
|
35
|
+
config: {},
|
|
36
|
+
} as const;
|
|
37
|
+
|
|
38
|
+
const VALID_EXTERNAL_STUDIO = {
|
|
39
|
+
id: "studio-002",
|
|
40
|
+
appHost: "https://my-studio.example.com",
|
|
41
|
+
createdAt: "2025-01-01T00:00:00Z",
|
|
42
|
+
updatedAt: "2025-01-01T00:00:00Z",
|
|
43
|
+
dashboardStatus: "default",
|
|
44
|
+
title: "My External Studio",
|
|
45
|
+
projectId: "proj-001",
|
|
46
|
+
type: "studio",
|
|
47
|
+
urlType: "external",
|
|
48
|
+
manifest: null,
|
|
49
|
+
manifestData: null,
|
|
50
|
+
autoUpdatingVersion: null,
|
|
51
|
+
activeDeployment: null,
|
|
52
|
+
config: {},
|
|
53
|
+
} as const;
|
|
54
|
+
|
|
55
|
+
describe("parseStudioUserApplication", () => {
|
|
56
|
+
it("parses a valid internal studio application", () => {
|
|
57
|
+
const studio = parseStudioUserApplication(VALID_INTERNAL_STUDIO);
|
|
58
|
+
expect(studio.urlType).toBe("internal");
|
|
59
|
+
expect(studio.activeDeployment?.version).toBe("4.0.0");
|
|
60
|
+
expectTypeOf(studio).toEqualTypeOf<StudioUserApplication>();
|
|
61
|
+
expectTypeOf(studio.id).toEqualTypeOf<UserApplicationId>();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("parses a valid external studio application", () => {
|
|
65
|
+
const studio = parseStudioUserApplication(VALID_EXTERNAL_STUDIO);
|
|
66
|
+
expect(studio.urlType).toBe("external");
|
|
67
|
+
expect(studio.activeDeployment).toBeNull();
|
|
68
|
+
expect(studio.title).toBe("My External Studio");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("rejects missing required fields", () => {
|
|
72
|
+
expect(() => parseStudioUserApplication({ id: "studio-001" })).toThrow();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("rejects an empty id", () => {
|
|
76
|
+
expect(() =>
|
|
77
|
+
parseStudioUserApplication({ ...VALID_INTERNAL_STUDIO, id: "" }),
|
|
78
|
+
).toThrow();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("rejects invalid urlType", () => {
|
|
82
|
+
expect(() =>
|
|
83
|
+
parseStudioUserApplication({
|
|
84
|
+
...VALID_INTERNAL_STUDIO,
|
|
85
|
+
urlType: "unknown",
|
|
86
|
+
}),
|
|
87
|
+
).toThrow();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("parses studio with manifestData", () => {
|
|
91
|
+
const studio = parseStudioUserApplication({
|
|
92
|
+
...VALID_INTERNAL_STUDIO,
|
|
93
|
+
manifestData: {
|
|
94
|
+
value: {
|
|
95
|
+
version: 2,
|
|
96
|
+
createdAt: "2025-01-01T00:00:00Z",
|
|
97
|
+
workspaces: [
|
|
98
|
+
{
|
|
99
|
+
name: "default",
|
|
100
|
+
title: "Default",
|
|
101
|
+
basePath: "/",
|
|
102
|
+
projectId: "proj-001",
|
|
103
|
+
dataset: "production",
|
|
104
|
+
schema: "schema.json",
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
expect(studio.manifestData?.value.workspaces).toHaveLength(1);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
import { ProjectId } from "../../projects";
|
|
4
|
+
import { ActiveDeployment, UserApplicationBase } from "../user-application";
|
|
5
|
+
|
|
6
|
+
const Workspace = z.object({
|
|
7
|
+
name: z.string(),
|
|
8
|
+
title: z.string(),
|
|
9
|
+
subtitle: z.string().optional(),
|
|
10
|
+
basePath: z.string(),
|
|
11
|
+
projectId: ProjectId,
|
|
12
|
+
dataset: z.string().optional(),
|
|
13
|
+
icon: z.string().nullable().optional(),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @public
|
|
18
|
+
*/
|
|
19
|
+
export type Workspace = z.output<typeof Workspace>;
|
|
20
|
+
|
|
21
|
+
const ServerManifest = z.object({
|
|
22
|
+
buildId: z.string().optional(),
|
|
23
|
+
bundleVersion: z.string().optional(),
|
|
24
|
+
version: z.string().optional(),
|
|
25
|
+
workspaces: z
|
|
26
|
+
.array(
|
|
27
|
+
Workspace.extend({
|
|
28
|
+
dataset: z.string(),
|
|
29
|
+
schemaDescriptorId: z.string(),
|
|
30
|
+
}),
|
|
31
|
+
)
|
|
32
|
+
.optional(),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @public
|
|
37
|
+
*/
|
|
38
|
+
export const ClientManifest = z.object({
|
|
39
|
+
version: z.number(),
|
|
40
|
+
createdAt: z.string(),
|
|
41
|
+
studioVersion: z.string().optional(),
|
|
42
|
+
workspaces: z.array(
|
|
43
|
+
Workspace.extend({
|
|
44
|
+
schema: z.string(),
|
|
45
|
+
tools: z.string().optional(),
|
|
46
|
+
}),
|
|
47
|
+
),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const StudioUserApplicationBase = UserApplicationBase.extend({
|
|
51
|
+
title: z.string().nullable(),
|
|
52
|
+
projectId: ProjectId,
|
|
53
|
+
type: z.literal("studio"),
|
|
54
|
+
manifest: ClientManifest.nullable(),
|
|
55
|
+
manifestData: z.object({ value: ClientManifest }).nullable(),
|
|
56
|
+
autoUpdatingVersion: z.string().nullable(),
|
|
57
|
+
config: z.object({
|
|
58
|
+
"live-manifest": z
|
|
59
|
+
.object({
|
|
60
|
+
createdAt: z.string(),
|
|
61
|
+
updatedAt: z.string(),
|
|
62
|
+
updatedBy: z.string(),
|
|
63
|
+
value: ServerManifest,
|
|
64
|
+
})
|
|
65
|
+
.optional(),
|
|
66
|
+
}),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const InternalStudioUserApplication = StudioUserApplicationBase.extend({
|
|
70
|
+
urlType: z.literal("internal"),
|
|
71
|
+
activeDeployment: ActiveDeployment.extend({
|
|
72
|
+
manifest: ServerManifest.nullable(),
|
|
73
|
+
}),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const ExternalStudioUserApplication = StudioUserApplicationBase.extend({
|
|
77
|
+
urlType: z.literal("external"),
|
|
78
|
+
activeDeployment: z.null(),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Studio user application schema — validates and brands API
|
|
83
|
+
* responses from the `/user-applications?appType=studio`
|
|
84
|
+
* endpoint. Uses a discriminated union on `urlType`.
|
|
85
|
+
* @public
|
|
86
|
+
*/
|
|
87
|
+
export const StudioUserApplication = z.discriminatedUnion("urlType", [
|
|
88
|
+
InternalStudioUserApplication,
|
|
89
|
+
ExternalStudioUserApplication,
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @public
|
|
94
|
+
*/
|
|
95
|
+
export type StudioUserApplication = z.output<typeof StudioUserApplication>;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Validates and parses a raw API response into a branded
|
|
99
|
+
* StudioUserApplication.
|
|
100
|
+
* @public
|
|
101
|
+
*/
|
|
102
|
+
export function parseStudioUserApplication(
|
|
103
|
+
data: unknown,
|
|
104
|
+
): StudioUserApplication {
|
|
105
|
+
return StudioUserApplication.parse(data);
|
|
106
|
+
}
|