@sanity/workbench 0.1.0-alpha.7 → 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.
@@ -1,8 +1,14 @@
1
+ // eslint-disable-next-line no-restricted-imports
1
2
  import type { ApplicationResource as ProtocolApplicationResource } from "@sanity/message-protocol";
2
3
  import { afterEach, describe, expect, expectTypeOf, it, vi } from "vitest";
3
4
 
4
5
  import { APPLICATION_DATA } from "../__tests__/__fixtures__";
5
- import { CoreAppApplication } from "./core-app";
6
+ import {
7
+ CoreAppApplication,
8
+ parseCoreApplication,
9
+ type CoreAppUserApplication,
10
+ } from "./core-app";
11
+ import type { UserApplicationId } from "./user-application";
6
12
 
7
13
  const setup = () => new CoreAppApplication(APPLICATION_DATA);
8
14
 
@@ -73,7 +79,6 @@ describe("CoreAppApplication", () => {
73
79
  "dashboardStatus": "default",
74
80
  "id": "v27rvqtlp3lmdvcln6ey3lro",
75
81
  "organizationId": "oSyH1iET5",
76
- "projectId": null,
77
82
  "title": "sdk-movie-list",
78
83
  "type": "application",
79
84
  "updatedAt": "2025-03-27T19:00:32.792Z",
@@ -106,35 +111,35 @@ describe("CoreAppApplication", () => {
106
111
 
107
112
  describe("manifest functionality", () => {
108
113
  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",
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
+ },
117
124
  },
118
- },
119
- };
120
- const application = new CoreAppApplication(applicationWithManifest);
125
+ }),
126
+ );
121
127
  expect(application.title).toBe("Manifest Title");
122
128
  });
123
129
 
124
130
  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
131
  const application = new CoreAppApplication(
137
- applicationWithDeploymentManifest,
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
+ }),
138
143
  );
139
144
  expect(application.title).toBe("Deployment Title");
140
145
  });
@@ -149,3 +154,83 @@ describe("CoreAppApplication", () => {
149
154
  });
150
155
  });
151
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
+ });
@@ -1,21 +1,75 @@
1
- import type {
2
- ApplicationResource as ProtocolApplicationResource,
3
- CoreApplication,
4
- } from "@sanity/message-protocol";
1
+ // eslint-disable-next-line no-restricted-imports
2
+ import type { ApplicationResource as ProtocolApplicationResource } from "@sanity/message-protocol";
3
+ import { z } from "zod";
5
4
 
6
- import { UserApplication } from "./user-application";
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
+ }
7
61
 
8
62
  /**
9
63
  * @internal
10
64
  */
11
65
  export class CoreAppApplication extends UserApplication<
12
- CoreApplication,
66
+ CoreAppUserApplication,
13
67
  "coreApp",
14
68
  ProtocolApplicationResource
15
69
  > {
16
- readonly activeDeployment: CoreApplication["activeDeployment"];
70
+ readonly activeDeployment: CoreAppUserApplication["activeDeployment"];
17
71
 
18
- constructor(application: CoreApplication) {
72
+ constructor(application: CoreAppUserApplication) {
19
73
  super(application, "coreApp");
20
74
 
21
75
  this.activeDeployment = application.activeDeployment;
@@ -37,7 +91,9 @@ export class CoreAppApplication extends UserApplication<
37
91
  return this.application.updatedAt;
38
92
  }
39
93
 
40
- get<TKey extends keyof CoreApplication>(attr: TKey): CoreApplication[TKey] {
94
+ get<TKey extends keyof CoreAppUserApplication>(
95
+ attr: TKey,
96
+ ): CoreAppUserApplication[TKey] {
41
97
  if (!(attr in this.application)) {
42
98
  throw new Error(
43
99
  `Attribute ${attr.toString()} does not exist on application ${this.application.id}`,
@@ -52,6 +108,6 @@ export class CoreAppApplication extends UserApplication<
52
108
  ...this.application,
53
109
  type: "application",
54
110
  url: this.url.toString(),
55
- } satisfies ProtocolApplicationResource;
111
+ } as ProtocolApplicationResource;
56
112
  }
57
113
  }
@@ -0,0 +1,3 @@
1
+ export * from "./schemas";
2
+ export * from "./studio";
3
+ export * from "./workspace";
@@ -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
+ }