@jskit-ai/workspaces-core 0.1.223 → 0.1.225
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/package.json +9 -9
- package/patterns/workspace-server/PATTERN.md +6 -0
- package/src/server/WorkspacesFeature.js +18 -1
- package/src/server/WorkspacesIntegrationsProvider.js +22 -1
- package/src/server/common/contributors/workspaceActionContextContributor.js +2 -4
- package/src/server/common/formatters/workspaceFormatter.js +1 -1
- package/src/server/common/services/workspaceContextService.js +4 -2
- package/src/server/workspaceBootstrapContributor.js +2 -2
- package/src/server/workspaceDirectory/workspaceDirectoryActions.js +1 -17
- package/src/server/workspaceMembers/workspaceMembersActions.js +0 -28
- package/src/server/workspaceMembers/workspaceMembersService.js +1 -1
- package/src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js +1 -13
- package/src/server/workspacePendingInvitations/workspacePendingInvitationsService.js +1 -1
- package/src/server/workspaceSettings/workspaceSettingsActions.js +0 -8
- package/src/server/workspaceSettings/workspaceSettingsRepository.js +11 -29
- package/src/shared/resources/workspaceSettingsResource.js +7 -12
- package/src/shared/settings.js +12 -0
- package/src/shared/tenancyProfile.js +3 -2
- package/test/featureRuntime.test.js +52 -1
- package/test/resourcesCanonical.test.js +2 -2
- package/test/tenancyProfile.test.js +16 -0
- package/test/usersRouteResources.test.js +3 -3
- package/test/workspaceInternalCrudResources.test.js +2 -2
- package/test/workspaceMembersResource.test.js +2 -2
- package/test/workspacePendingInvitationsResource.test.js +4 -4
- package/test/workspaceService.test.js +28 -0
- package/test/workspaceSettingsRepository.test.js +19 -0
- package/test/workspaceSettingsResource.test.js +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/workspaces-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.225",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test --test-concurrency=1"
|
|
@@ -18,11 +18,11 @@
|
|
|
18
18
|
"./shared/resources/workspaceSettingsResource": "./src/shared/resources/workspaceSettingsResource.js"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@jskit-ai/database-runtime": "0.1.
|
|
22
|
-
"@jskit-ai/json-rest-api-core": "0.1.
|
|
23
|
-
"@jskit-ai/resource-core": "0.1.
|
|
24
|
-
"@jskit-ai/resource-crud-core": "0.1.
|
|
25
|
-
"@jskit-ai/users-core": "0.1.
|
|
21
|
+
"@jskit-ai/database-runtime": "0.1.247",
|
|
22
|
+
"@jskit-ai/json-rest-api-core": "0.1.191",
|
|
23
|
+
"@jskit-ai/resource-core": "0.1.189",
|
|
24
|
+
"@jskit-ai/resource-crud-core": "0.1.189",
|
|
25
|
+
"@jskit-ai/users-core": "0.1.260",
|
|
26
26
|
"json-rest-schema": "^1.0.17"
|
|
27
27
|
},
|
|
28
28
|
"description": "Workspace tenancy runtime plus HTTP routes, role catalog, and workspace config scaffolding.",
|
|
@@ -169,8 +169,8 @@
|
|
|
169
169
|
}
|
|
170
170
|
},
|
|
171
171
|
"peerDependencies": {
|
|
172
|
-
"@jskit-ai/auth-core": "0.1.
|
|
173
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
174
|
-
"@jskit-ai/kernel": "0.1.
|
|
172
|
+
"@jskit-ai/auth-core": "0.1.245",
|
|
173
|
+
"@jskit-ai/http-runtime": "0.1.245",
|
|
174
|
+
"@jskit-ai/kernel": "0.1.247"
|
|
175
175
|
}
|
|
176
176
|
}
|
|
@@ -23,6 +23,12 @@ substitute for a simpler domain ownership column.
|
|
|
23
23
|
Choose workspace creation policy, roles, invitation policy, expiry, email copy,
|
|
24
24
|
member permissions, personal-workspace behavior, and surface access rules.
|
|
25
25
|
|
|
26
|
+
In `workspaces` mode, `tenancyPolicy.workspace.autoProvision: true` gives every
|
|
27
|
+
user a personal workspace in addition to shared ones; the server config function
|
|
28
|
+
`personalWorkspaceName(user)` names it. `workspaceSettings.defaults` sets
|
|
29
|
+
`invitesEnabled` and the colours (`lightPrimaryColor`, `darkPrimaryColor` and
|
|
30
|
+
the other palette fields) that new workspaces start with.
|
|
31
|
+
|
|
26
32
|
## Framework APIs
|
|
27
33
|
|
|
28
34
|
Use workspace routes, resources, scope helpers, tenancy profiles, and settings
|
|
@@ -9,6 +9,7 @@ import { workspaceMembershipsResource } from "../shared/resources/workspaceMembe
|
|
|
9
9
|
import { workspaceResource } from "../shared/resources/workspaceResource.js";
|
|
10
10
|
import { workspaceSettingsResource } from "../shared/resources/workspaceSettingsResource.js";
|
|
11
11
|
import { createWorkspaceRoleCatalog } from "../shared/roles.js";
|
|
12
|
+
import { WORKSPACE_SETTINGS_COLOR_FIELDS, normalizeWorkspaceHexColor } from "../shared/settings.js";
|
|
12
13
|
import { resolveTenancyProfile } from "../shared/tenancyProfile.js";
|
|
13
14
|
import { createRepository as createWorkspaceInvitesRepository } from "./common/repositories/workspaceInvitesRepository.js";
|
|
14
15
|
import { createRepository as createWorkspaceMembershipsRepository } from "./common/repositories/workspaceMembershipsRepository.js";
|
|
@@ -49,6 +50,21 @@ function requireBoolean(value, label) {
|
|
|
49
50
|
return value;
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
function requireWorkspaceColorDefaults(defaults) {
|
|
54
|
+
const colors = {};
|
|
55
|
+
for (const fieldName of WORKSPACE_SETTINGS_COLOR_FIELDS) {
|
|
56
|
+
if (defaults?.[fieldName] === undefined) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const color = normalizeWorkspaceHexColor(defaults[fieldName]);
|
|
60
|
+
if (!color) {
|
|
61
|
+
throw new TypeError(`runtime.config.workspaceSettings.defaults.${fieldName} must be a hex color like #1867C0.`);
|
|
62
|
+
}
|
|
63
|
+
colors[fieldName] = color;
|
|
64
|
+
}
|
|
65
|
+
return colors;
|
|
66
|
+
}
|
|
67
|
+
|
|
52
68
|
function resolveInviteEmailTemplate(config) {
|
|
53
69
|
const template =
|
|
54
70
|
config.workspaceInviteEmailTemplate ||
|
|
@@ -92,7 +108,8 @@ function createWorkspacesRuntime({ config, env, jsonRestApi } = {}) {
|
|
|
92
108
|
defaultInvitesEnabled: requireBoolean(
|
|
93
109
|
config?.workspaceSettings?.defaults?.invitesEnabled,
|
|
94
110
|
"runtime.config.workspaceSettings.defaults.invitesEnabled"
|
|
95
|
-
)
|
|
111
|
+
),
|
|
112
|
+
defaultColorSettings: requireWorkspaceColorDefaults(config?.workspaceSettings?.defaults)
|
|
96
113
|
});
|
|
97
114
|
const workspacesRepository = createWorkspacesRepository({ api: jsonRestApi });
|
|
98
115
|
const workspaceMembershipsRepository = createWorkspaceMembershipsRepository({ api: jsonRestApi });
|
|
@@ -7,6 +7,16 @@ import {
|
|
|
7
7
|
resolveWorkspaceSurfaceIdsFromAppConfig
|
|
8
8
|
} from "./support/workspaceActionSurfaces.js";
|
|
9
9
|
|
|
10
|
+
// Every active membership puts the actor in the workspace audience and in its
|
|
11
|
+
// own workspace-user audience.
|
|
12
|
+
async function resolveWorkspaceRealtimeScopes(workspaceMemberships, actorId) {
|
|
13
|
+
const workspaceIds = await workspaceMemberships.listActiveWorkspaceIdsByUserId(actorId);
|
|
14
|
+
return workspaceIds.flatMap((workspaceId) => [
|
|
15
|
+
{ kind: "workspace", id: workspaceId },
|
|
16
|
+
{ kind: "workspace_user", workspaceId, userId: actorId }
|
|
17
|
+
]);
|
|
18
|
+
}
|
|
19
|
+
|
|
10
20
|
const WorkspacesIntegrationsProvider = defineProvider({
|
|
11
21
|
id: "workspaces.integrations",
|
|
12
22
|
requires: {
|
|
@@ -17,7 +27,10 @@ const WorkspacesIntegrationsProvider = defineProvider({
|
|
|
17
27
|
usersExtensions: "users.extensions",
|
|
18
28
|
workspaces: "workspaces.core"
|
|
19
29
|
},
|
|
20
|
-
|
|
30
|
+
optional: {
|
|
31
|
+
realtime: "runtime.realtime"
|
|
32
|
+
},
|
|
33
|
+
setup({ actions, authExtensions, config, http, realtime, usersExtensions, workspaces }) {
|
|
21
34
|
const workspaceService = workspaces.services.directory;
|
|
22
35
|
const membershipOptionalSurfaceIds = resolveWorkspaceMembershipOptionalSurfaceIdsFromAppConfig(config);
|
|
23
36
|
const workspaceSurfaceIds = resolveWorkspaceSurfaceIdsFromAppConfig(config);
|
|
@@ -58,6 +71,14 @@ const WorkspacesIntegrationsProvider = defineProvider({
|
|
|
58
71
|
}
|
|
59
72
|
});
|
|
60
73
|
|
|
74
|
+
realtime?.registerAudienceContributor({
|
|
75
|
+
id: "workspaces.memberships",
|
|
76
|
+
resolveActorScopes: (actorId) => resolveWorkspaceRealtimeScopes(
|
|
77
|
+
workspaces.repositories.workspaceMemberships,
|
|
78
|
+
actorId
|
|
79
|
+
)
|
|
80
|
+
});
|
|
81
|
+
|
|
61
82
|
if (workspaces.invitationsEnabled) {
|
|
62
83
|
authExtensions.registerInvitationContextResolver({
|
|
63
84
|
resolverId: "workspaces.invitation",
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
requireServiceMethod
|
|
4
|
-
} from "@jskit-ai/kernel/shared/actions/actionContributorHelpers";
|
|
1
|
+
import { requireServiceMethod } from "@jskit-ai/kernel/server/actions/actionContributorHelpers";
|
|
2
|
+
import { normalizeObject } from "@jskit-ai/kernel/shared/support/normalize";
|
|
5
3
|
import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
|
|
6
4
|
import {
|
|
7
5
|
checkRouteVisibility,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolveWorkspaceThemePalettes } from "@jskit-ai/workspaces-core/shared/settings";
|
|
2
|
-
import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/
|
|
2
|
+
import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
3
3
|
import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
|
|
4
4
|
|
|
5
5
|
function mapWorkspaceSummary(workspace, membership, roleCatalog = null) {
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
import {
|
|
12
12
|
mapWorkspaceSummary
|
|
13
13
|
} from "../formatters/workspaceFormatter.js";
|
|
14
|
-
import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/
|
|
14
|
+
import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
15
15
|
import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
|
|
16
16
|
|
|
17
17
|
function toSlugPart(value) {
|
|
@@ -84,6 +84,8 @@ function createService({
|
|
|
84
84
|
const resolvedTenancyProfile = resolveTenancyProfile(appConfig);
|
|
85
85
|
const resolvedTenancyMode = resolvedTenancyProfile.mode;
|
|
86
86
|
const workspacePolicy = resolvedTenancyProfile.workspace;
|
|
87
|
+
const personalWorkspaceName =
|
|
88
|
+
typeof appConfig.personalWorkspaceName === "function" ? appConfig.personalWorkspaceName : null;
|
|
87
89
|
async function ensureUniqueWorkspaceSlug(baseSlug, options = {}) {
|
|
88
90
|
let suffix = 0;
|
|
89
91
|
while (suffix < 1000) {
|
|
@@ -121,7 +123,7 @@ function createService({
|
|
|
121
123
|
const inserted = await workspacesRepository.insert(
|
|
122
124
|
{
|
|
123
125
|
slug,
|
|
124
|
-
name: buildWorkspaceName(user),
|
|
126
|
+
name: normalizeText(personalWorkspaceName?.(user)) || buildWorkspaceName(user),
|
|
125
127
|
ownerUserId: normalizedUserId,
|
|
126
128
|
isPersonal: true,
|
|
127
129
|
avatarUrl: ""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { requireServiceMethod } from "@jskit-ai/kernel/
|
|
1
|
+
import { requireServiceMethod } from "@jskit-ai/kernel/server/actions/actionContributorHelpers";
|
|
2
2
|
import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
|
|
3
|
-
import { normalizeLowerText } from "@jskit-ai/kernel/shared/
|
|
3
|
+
import { normalizeLowerText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
4
4
|
import {
|
|
5
5
|
TENANCY_MODE_NONE,
|
|
6
6
|
resolveTenancyProfile
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
emptyInputValidator,
|
|
3
3
|
resolveRequest
|
|
4
|
-
} from "@jskit-ai/kernel/
|
|
4
|
+
} from "@jskit-ai/kernel/server/actions/actionContributorHelpers";
|
|
5
5
|
import { returnJsonApiData } from "@jskit-ai/http-runtime/shared";
|
|
6
6
|
import { composeSchemaDefinitions } from "@jskit-ai/kernel/shared/validators";
|
|
7
7
|
import { workspaceResource } from "../../shared/resources/workspaceResource.js";
|
|
@@ -40,10 +40,6 @@ const workspaceDirectoryActionSpecifications = Object.freeze([
|
|
|
40
40
|
}
|
|
41
41
|
},
|
|
42
42
|
idempotency: "none",
|
|
43
|
-
audit: {
|
|
44
|
-
actionName: "workspace.workspaces.create"
|
|
45
|
-
},
|
|
46
|
-
observability: {},
|
|
47
43
|
async run(workspaceService, input, context) {
|
|
48
44
|
return returnJsonApiData(await workspaceService.createWorkspaceForAuthenticatedUser(resolveActionUser(context, input), input, {
|
|
49
45
|
request: resolveRequest(context),
|
|
@@ -74,10 +70,6 @@ const workspaceDirectoryActionSpecifications = Object.freeze([
|
|
|
74
70
|
}
|
|
75
71
|
},
|
|
76
72
|
idempotency: "none",
|
|
77
|
-
audit: {
|
|
78
|
-
actionName: "workspace.workspaces.list"
|
|
79
|
-
},
|
|
80
|
-
observability: {},
|
|
81
73
|
async run(workspaceService, input, context) {
|
|
82
74
|
return returnJsonApiData({
|
|
83
75
|
items: await workspaceService.listWorkspacesForAuthenticatedUser(resolveActionUser(context, input), {
|
|
@@ -108,10 +100,6 @@ const workspaceDirectoryActionSpecifications = Object.freeze([
|
|
|
108
100
|
}
|
|
109
101
|
},
|
|
110
102
|
idempotency: "none",
|
|
111
|
-
audit: {
|
|
112
|
-
actionName: "workspace.workspaces.read"
|
|
113
|
-
},
|
|
114
|
-
observability: {},
|
|
115
103
|
async run(workspaceService, input, context) {
|
|
116
104
|
return returnJsonApiData(await workspaceService.getWorkspaceForAuthenticatedUser(
|
|
117
105
|
resolveActionUser(context, input),
|
|
@@ -143,10 +131,6 @@ const workspaceDirectoryActionSpecifications = Object.freeze([
|
|
|
143
131
|
}
|
|
144
132
|
},
|
|
145
133
|
idempotency: "optional",
|
|
146
|
-
audit: {
|
|
147
|
-
actionName: "workspace.workspaces.update"
|
|
148
|
-
},
|
|
149
|
-
observability: {},
|
|
150
134
|
async run(workspaceService, input, context) {
|
|
151
135
|
const { workspaceSlug, ...patch } = input;
|
|
152
136
|
return returnJsonApiData(await workspaceService.updateWorkspaceForAuthenticatedUser(
|
|
@@ -85,10 +85,6 @@ const workspaceMembersActionSpecifications = Object.freeze([
|
|
|
85
85
|
}
|
|
86
86
|
},
|
|
87
87
|
idempotency: "none",
|
|
88
|
-
audit: {
|
|
89
|
-
actionName: "workspace.roles.list"
|
|
90
|
-
},
|
|
91
|
-
observability: {},
|
|
92
88
|
async run(workspaceMembersService, _input, context) {
|
|
93
89
|
return returnJsonApiData(await workspaceMembersService.listRoles({ context }));
|
|
94
90
|
}
|
|
@@ -113,10 +109,6 @@ const workspaceMembersActionSpecifications = Object.freeze([
|
|
|
113
109
|
}
|
|
114
110
|
},
|
|
115
111
|
idempotency: "none",
|
|
116
|
-
audit: {
|
|
117
|
-
actionName: "workspace.members.list"
|
|
118
|
-
},
|
|
119
|
-
observability: {},
|
|
120
112
|
async run(workspaceMembersService, input, context) {
|
|
121
113
|
return returnJsonApiData(await workspaceMembersService.listMembers(resolveWorkspace(context, input), {
|
|
122
114
|
context
|
|
@@ -143,10 +135,6 @@ const workspaceMembersActionSpecifications = Object.freeze([
|
|
|
143
135
|
}
|
|
144
136
|
},
|
|
145
137
|
idempotency: "optional",
|
|
146
|
-
audit: {
|
|
147
|
-
actionName: "workspace.member.role.update"
|
|
148
|
-
},
|
|
149
|
-
observability: {},
|
|
150
138
|
events: WORKSPACE_MEMBER_CHANGED_EVENTS,
|
|
151
139
|
async run(workspaceMembersService, input, context) {
|
|
152
140
|
return returnJsonApiData(await workspaceMembersService.updateMemberRole(resolveWorkspace(context, input), {
|
|
@@ -177,10 +165,6 @@ const workspaceMembersActionSpecifications = Object.freeze([
|
|
|
177
165
|
}
|
|
178
166
|
},
|
|
179
167
|
idempotency: "optional",
|
|
180
|
-
audit: {
|
|
181
|
-
actionName: "workspace.member.remove"
|
|
182
|
-
},
|
|
183
|
-
observability: {},
|
|
184
168
|
events: WORKSPACE_MEMBER_CHANGED_EVENTS,
|
|
185
169
|
async run(workspaceMembersService, input, context) {
|
|
186
170
|
return returnJsonApiData(await workspaceMembersService.removeMember(resolveWorkspace(context, input), {
|
|
@@ -210,10 +194,6 @@ const workspaceMembersActionSpecifications = Object.freeze([
|
|
|
210
194
|
}
|
|
211
195
|
},
|
|
212
196
|
idempotency: "none",
|
|
213
|
-
audit: {
|
|
214
|
-
actionName: "workspace.invites.list"
|
|
215
|
-
},
|
|
216
|
-
observability: {},
|
|
217
197
|
async run(workspaceMembersService, input, context) {
|
|
218
198
|
return returnJsonApiData(await workspaceMembersService.listInvites(resolveWorkspace(context, input), {
|
|
219
199
|
context
|
|
@@ -245,10 +225,6 @@ const workspaceMembersActionSpecifications = Object.freeze([
|
|
|
245
225
|
}
|
|
246
226
|
},
|
|
247
227
|
idempotency: "optional",
|
|
248
|
-
audit: {
|
|
249
|
-
actionName: "workspace.invite.create"
|
|
250
|
-
},
|
|
251
|
-
observability: {},
|
|
252
228
|
events: WORKSPACE_INVITE_CREATED_EVENTS,
|
|
253
229
|
async run(workspaceMembersService, input, context) {
|
|
254
230
|
return returnJsonApiData(await workspaceMembersService.createInvite(
|
|
@@ -284,10 +260,6 @@ const workspaceMembersActionSpecifications = Object.freeze([
|
|
|
284
260
|
}
|
|
285
261
|
},
|
|
286
262
|
idempotency: "optional",
|
|
287
|
-
audit: {
|
|
288
|
-
actionName: "workspace.invite.revoke"
|
|
289
|
-
},
|
|
290
|
-
observability: {},
|
|
291
263
|
events: WORKSPACE_INVITE_REVOKED_EVENTS,
|
|
292
264
|
async run(workspaceMembersService, input, context) {
|
|
293
265
|
return returnJsonApiData(await workspaceMembersService.revokeInvite(resolveWorkspace(context, input), input.inviteId, {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
-
import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/
|
|
2
|
+
import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
3
3
|
import { encodeInviteTokenHash } from "@jskit-ai/auth-core/shared/inviteTokens";
|
|
4
4
|
import { resolveInviteTokenHash, buildInviteToken, hashInviteToken } from "@jskit-ai/auth-core/server/inviteTokens";
|
|
5
5
|
import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { pendingInvitationsAssistantOutput } from "../../shared/resources/workspaceAssistantOutputs.js";
|
|
2
2
|
import {
|
|
3
3
|
emptyInputValidator
|
|
4
|
-
} from "@jskit-ai/kernel/
|
|
4
|
+
} from "@jskit-ai/kernel/server/actions/actionContributorHelpers";
|
|
5
5
|
import { returnJsonApiData } from "@jskit-ai/http-runtime/shared";
|
|
6
6
|
import { workspaceMembersResource } from "../../shared/resources/workspaceMembersResource.js";
|
|
7
7
|
import { workspacePendingInvitationsResource } from "../../shared/resources/workspacePendingInvitationsResource.js";
|
|
@@ -28,10 +28,6 @@ const workspacePendingInvitationsActionSpecifications = Object.freeze([
|
|
|
28
28
|
}
|
|
29
29
|
},
|
|
30
30
|
idempotency: "none",
|
|
31
|
-
audit: {
|
|
32
|
-
actionName: "workspace.invitation.resolve"
|
|
33
|
-
},
|
|
34
|
-
observability: {},
|
|
35
31
|
async run(workspacePendingInvitationsService, input, context) {
|
|
36
32
|
return returnJsonApiData(
|
|
37
33
|
await workspacePendingInvitationsService.resolveInviteByToken(input?.token, {
|
|
@@ -64,10 +60,6 @@ const workspacePendingInvitationsActionSpecifications = Object.freeze([
|
|
|
64
60
|
}
|
|
65
61
|
},
|
|
66
62
|
idempotency: "none",
|
|
67
|
-
audit: {
|
|
68
|
-
actionName: "workspace.invitations.pending.list"
|
|
69
|
-
},
|
|
70
|
-
observability: {},
|
|
71
63
|
async run(workspacePendingInvitationsService, input, context) {
|
|
72
64
|
return returnJsonApiData({
|
|
73
65
|
pendingInvites: await workspacePendingInvitationsService.listPendingInvitesForUser(resolveActionUser(context, input), {
|
|
@@ -93,10 +85,6 @@ const workspacePendingInvitationsActionSpecifications = Object.freeze([
|
|
|
93
85
|
}
|
|
94
86
|
},
|
|
95
87
|
idempotency: "optional",
|
|
96
|
-
audit: {
|
|
97
|
-
actionName: "workspace.invite.redeem"
|
|
98
|
-
},
|
|
99
|
-
observability: {},
|
|
100
88
|
events: WORKSPACE_INVITE_DECISION_EVENTS,
|
|
101
89
|
async run(workspacePendingInvitationsService, input, context) {
|
|
102
90
|
const payload = input || {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { resolveInviteTokenHash } from "@jskit-ai/auth-core/server/inviteTokens";
|
|
2
2
|
import { encodeInviteTokenHash } from "@jskit-ai/auth-core/shared/inviteTokens";
|
|
3
3
|
import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
|
|
4
|
-
import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/
|
|
4
|
+
import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
5
5
|
import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
|
|
6
6
|
|
|
7
7
|
function createService({
|
|
@@ -40,10 +40,6 @@ const workspaceSettingsActionSpecifications = Object.freeze([
|
|
|
40
40
|
}
|
|
41
41
|
},
|
|
42
42
|
idempotency: "none",
|
|
43
|
-
audit: {
|
|
44
|
-
actionName: "workspace.settings.read"
|
|
45
|
-
},
|
|
46
|
-
observability: {},
|
|
47
43
|
async run(workspaceSettingsService, input, context) {
|
|
48
44
|
const response = await workspaceSettingsService.getWorkspaceSettings(resolveWorkspace(context, input), {
|
|
49
45
|
context
|
|
@@ -72,10 +68,6 @@ const workspaceSettingsActionSpecifications = Object.freeze([
|
|
|
72
68
|
}
|
|
73
69
|
},
|
|
74
70
|
idempotency: "optional",
|
|
75
|
-
audit: {
|
|
76
|
-
actionName: "workspace.settings.update"
|
|
77
|
-
},
|
|
78
|
-
observability: {},
|
|
79
71
|
events: WORKSPACE_SETTINGS_CHANGED_EVENTS,
|
|
80
72
|
async run(workspaceSettingsService, input, context) {
|
|
81
73
|
const { workspaceSlug, ...patch } = input;
|
|
@@ -6,34 +6,12 @@ import {
|
|
|
6
6
|
createJsonRestContext,
|
|
7
7
|
extractJsonRestCollectionRows
|
|
8
8
|
} from "@jskit-ai/json-rest-api-core/server/jsonRestApiHost";
|
|
9
|
+
import { pickOwnProperties } from "@jskit-ai/kernel/shared/support";
|
|
10
|
+
import { WORKSPACE_SETTINGS_FIELD_KEYS } from "../../shared/resources/workspaceSettingsResource.js";
|
|
9
11
|
import { resolveWorkspaceThemePalettes } from "../../shared/settings.js";
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"lightSecondaryColor",
|
|
14
|
-
"lightSurfaceColor",
|
|
15
|
-
"lightSurfaceVariantColor",
|
|
16
|
-
"darkPrimaryColor",
|
|
17
|
-
"darkSecondaryColor",
|
|
18
|
-
"darkSurfaceColor",
|
|
19
|
-
"darkSurfaceVariantColor",
|
|
20
|
-
"invitesEnabled"
|
|
21
|
-
]);
|
|
22
|
-
|
|
23
|
-
function pickPatchFields(source = {}) {
|
|
24
|
-
const patch = {};
|
|
25
|
-
|
|
26
|
-
for (const fieldName of WORKSPACE_SETTINGS_PATCH_FIELDS) {
|
|
27
|
-
if (Object.hasOwn(source, fieldName)) {
|
|
28
|
-
patch[fieldName] = source[fieldName];
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return patch;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function createDefaultWorkspaceSettingsCreatePayload(workspaceId, defaultInvitesEnabled) {
|
|
36
|
-
const palettes = resolveWorkspaceThemePalettes({});
|
|
13
|
+
function createDefaultWorkspaceSettingsCreatePayload(workspaceId, defaultInvitesEnabled, defaultColorSettings = {}) {
|
|
14
|
+
const palettes = resolveWorkspaceThemePalettes(defaultColorSettings);
|
|
37
15
|
|
|
38
16
|
return {
|
|
39
17
|
id: workspaceId,
|
|
@@ -49,7 +27,7 @@ function createDefaultWorkspaceSettingsCreatePayload(workspaceId, defaultInvites
|
|
|
49
27
|
};
|
|
50
28
|
}
|
|
51
29
|
|
|
52
|
-
function createRepository({ api, defaultInvitesEnabled = true } = {}) {
|
|
30
|
+
function createRepository({ api, defaultInvitesEnabled = true, defaultColorSettings = {} } = {}) {
|
|
53
31
|
if (!api?.resources?.workspaceSettings) {
|
|
54
32
|
throw new TypeError("workspaceSettingsRepository requires json-rest-api workspaceSettings resource.");
|
|
55
33
|
}
|
|
@@ -95,7 +73,11 @@ function createRepository({ api, defaultInvitesEnabled = true } = {}) {
|
|
|
95
73
|
try {
|
|
96
74
|
await api.resources.workspaceSettings.post(
|
|
97
75
|
{
|
|
98
|
-
data: createDefaultWorkspaceSettingsCreatePayload(
|
|
76
|
+
data: createDefaultWorkspaceSettingsCreatePayload(
|
|
77
|
+
normalizedWorkspaceId,
|
|
78
|
+
defaultInvitesEnabled === true,
|
|
79
|
+
defaultColorSettings
|
|
80
|
+
),
|
|
99
81
|
format: "plain",
|
|
100
82
|
returning: "full",
|
|
101
83
|
transaction: options?.trx || null
|
|
@@ -119,7 +101,7 @@ function createRepository({ api, defaultInvitesEnabled = true } = {}) {
|
|
|
119
101
|
|
|
120
102
|
await ensureForWorkspaceId(normalizedWorkspaceId, options);
|
|
121
103
|
const source = patch && typeof patch === "object" && !Array.isArray(patch) ? patch : {};
|
|
122
|
-
const updatePayload =
|
|
104
|
+
const updatePayload = pickOwnProperties(source, WORKSPACE_SETTINGS_FIELD_KEYS);
|
|
123
105
|
|
|
124
106
|
if (Object.keys(updatePayload).length < 1) {
|
|
125
107
|
return findByWorkspaceId(normalizedWorkspaceId, options);
|
|
@@ -3,19 +3,14 @@ import { deepFreeze } from "@jskit-ai/kernel/shared/support/deepFreeze";
|
|
|
3
3
|
import { RECORD_ID_PATTERN } from "@jskit-ai/kernel/shared/validators";
|
|
4
4
|
import { defineCrudResource } from "@jskit-ai/resource-crud-core/shared/crudResource";
|
|
5
5
|
import { workspaceRoleCatalogSchema } from "./workspaceRoleCatalogSchema.js";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
normalizeWorkspaceHexColor,
|
|
8
|
+
DEFAULT_WORKSPACE_DARK_PALETTE,
|
|
9
|
+
DEFAULT_WORKSPACE_LIGHT_PALETTE,
|
|
10
|
+
WORKSPACE_SETTINGS_COLOR_FIELDS
|
|
11
|
+
} from "../settings.js";
|
|
7
12
|
|
|
8
|
-
const WORKSPACE_SETTINGS_FIELD_KEYS = deepFreeze([
|
|
9
|
-
"lightPrimaryColor",
|
|
10
|
-
"lightSecondaryColor",
|
|
11
|
-
"lightSurfaceColor",
|
|
12
|
-
"lightSurfaceVariantColor",
|
|
13
|
-
"darkPrimaryColor",
|
|
14
|
-
"darkSecondaryColor",
|
|
15
|
-
"darkSurfaceColor",
|
|
16
|
-
"darkSurfaceVariantColor",
|
|
17
|
-
"invitesEnabled"
|
|
18
|
-
]);
|
|
13
|
+
const WORKSPACE_SETTINGS_FIELD_KEYS = deepFreeze([...WORKSPACE_SETTINGS_COLOR_FIELDS, "invitesEnabled"]);
|
|
19
14
|
|
|
20
15
|
const workspaceSettingsBodySchema = createSchema({
|
|
21
16
|
lightPrimaryColor: {
|
package/src/shared/settings.js
CHANGED
|
@@ -18,6 +18,17 @@ const DEFAULT_WORKSPACE_DARK_PALETTE = Object.freeze({
|
|
|
18
18
|
|
|
19
19
|
const DEFAULT_WORKSPACE_COLOR = DEFAULT_WORKSPACE_LIGHT_PALETTE.color;
|
|
20
20
|
|
|
21
|
+
const WORKSPACE_SETTINGS_COLOR_FIELDS = Object.freeze([
|
|
22
|
+
"lightPrimaryColor",
|
|
23
|
+
"lightSecondaryColor",
|
|
24
|
+
"lightSurfaceColor",
|
|
25
|
+
"lightSurfaceVariantColor",
|
|
26
|
+
"darkPrimaryColor",
|
|
27
|
+
"darkSecondaryColor",
|
|
28
|
+
"darkSurfaceColor",
|
|
29
|
+
"darkSurfaceVariantColor"
|
|
30
|
+
]);
|
|
31
|
+
|
|
21
32
|
function normalizeWorkspaceHexColor(value) {
|
|
22
33
|
const normalized = String(value || "").trim();
|
|
23
34
|
if (!HEX_COLOR_PATTERN.test(normalized)) {
|
|
@@ -113,6 +124,7 @@ export {
|
|
|
113
124
|
normalizeWorkspaceThemeMode,
|
|
114
125
|
resolveWorkspaceThemeDefaultPalette,
|
|
115
126
|
resolveWorkspaceThemePalettes,
|
|
127
|
+
WORKSPACE_SETTINGS_COLOR_FIELDS,
|
|
116
128
|
WORKSPACE_THEME_MODE_DARK,
|
|
117
129
|
WORKSPACE_THEME_MODE_LIGHT,
|
|
118
130
|
resolveWorkspaceThemePalette
|
|
@@ -15,7 +15,8 @@ function resolveWorkspacePolicyOverrides(appConfig = {}) {
|
|
|
15
15
|
const workspacePolicy = isRecord(tenancyPolicy.workspace) ? tenancyPolicy.workspace : {};
|
|
16
16
|
|
|
17
17
|
return Object.freeze({
|
|
18
|
-
allowSelfCreate: typeof workspacePolicy.allowSelfCreate === "boolean" ? workspacePolicy.allowSelfCreate : null
|
|
18
|
+
allowSelfCreate: typeof workspacePolicy.allowSelfCreate === "boolean" ? workspacePolicy.allowSelfCreate : null,
|
|
19
|
+
autoProvision: typeof workspacePolicy.autoProvision === "boolean" ? workspacePolicy.autoProvision : null
|
|
19
20
|
});
|
|
20
21
|
}
|
|
21
22
|
|
|
@@ -40,7 +41,7 @@ function resolveWorkspacePolicy(mode, overrides = {}) {
|
|
|
40
41
|
|
|
41
42
|
return Object.freeze({
|
|
42
43
|
enabled: true,
|
|
43
|
-
autoProvision:
|
|
44
|
+
autoProvision: overrides.autoProvision === true,
|
|
44
45
|
allowSelfCreate: overrides.allowSelfCreate === true,
|
|
45
46
|
slugPolicy: WORKSPACE_SLUG_POLICY_USER_SELECTED
|
|
46
47
|
});
|
|
@@ -5,7 +5,7 @@ import { createActionProvider } from "@jskit-ai/kernel/server/actions";
|
|
|
5
5
|
import { createCapabilityRuntime, defineProvider } from "@jskit-ai/kernel/shared/capabilities";
|
|
6
6
|
import { createBootstrapRuntime } from "@jskit-ai/kernel/server/runtime";
|
|
7
7
|
import { createUsersExtensions } from "@jskit-ai/users-core/server/usersExtensions";
|
|
8
|
-
import { WorkspacesFeature } from "../src/server/WorkspacesFeature.js";
|
|
8
|
+
import { WorkspacesFeature, createWorkspacesRuntime } from "../src/server/WorkspacesFeature.js";
|
|
9
9
|
import { WorkspacesIntegrationsProvider } from "../src/server/WorkspacesIntegrationsProvider.js";
|
|
10
10
|
|
|
11
11
|
function createJsonRestApiFixture() {
|
|
@@ -50,6 +50,7 @@ function runtimeConfig() {
|
|
|
50
50
|
test("WorkspacesFeature assembles workspace behavior and explicit integrations", async () => {
|
|
51
51
|
const routes = [];
|
|
52
52
|
const visibilityResolvers = [];
|
|
53
|
+
const audienceContributors = [];
|
|
53
54
|
const bootstrap = createBootstrapRuntime();
|
|
54
55
|
const authExtensions = createAuthExtensions();
|
|
55
56
|
const usersExtensions = createUsersExtensions();
|
|
@@ -86,6 +87,11 @@ test("WorkspacesFeature assembles workspace behavior and explicit integrations",
|
|
|
86
87
|
}
|
|
87
88
|
},
|
|
88
89
|
"runtime.json-rest-api": jsonRestApi,
|
|
90
|
+
"runtime.realtime": {
|
|
91
|
+
registerAudienceContributor(contributor) {
|
|
92
|
+
audienceContributors.push(contributor);
|
|
93
|
+
}
|
|
94
|
+
},
|
|
89
95
|
"users.core": {
|
|
90
96
|
repositories: {
|
|
91
97
|
userProfiles: { async findById() { return null; } }
|
|
@@ -117,4 +123,49 @@ test("WorkspacesFeature assembles workspace behavior and explicit integrations",
|
|
|
117
123
|
assert.deepEqual(usersExtensions.diagnostics().profileSyncLifecycleContributorIds, ["workspaces.provisioning"]);
|
|
118
124
|
assert.deepEqual(authExtensions.diagnostics().invitationContextResolverIds, ["workspaces.invitation"]);
|
|
119
125
|
assert.deepEqual(authExtensions.diagnostics().policyContextResolverIds, ["workspaces.policy"]);
|
|
126
|
+
assert.deepEqual(audienceContributors.map((entry) => entry.id), ["workspaces.memberships"]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("workspace realtime audiences are the actor's active memberships", async () => {
|
|
130
|
+
const audienceContributors = [];
|
|
131
|
+
const noop = () => {};
|
|
132
|
+
WorkspacesIntegrationsProvider.setup({
|
|
133
|
+
actions: { registerContextContributor: noop },
|
|
134
|
+
authExtensions: { registerPolicyContextResolver: noop },
|
|
135
|
+
config: runtimeConfig(),
|
|
136
|
+
http: { registerVisibilityResolver: noop },
|
|
137
|
+
realtime: { registerAudienceContributor: (entry) => audienceContributors.push(entry) },
|
|
138
|
+
usersExtensions: { registerProfileSyncLifecycleContributor: noop },
|
|
139
|
+
workspaces: {
|
|
140
|
+
invitationsEnabled: false,
|
|
141
|
+
services: { directory: { async resolveWorkspaceContextForUserBySlug() { return null; } } },
|
|
142
|
+
repositories: {
|
|
143
|
+
workspaceMemberships: {
|
|
144
|
+
async listActiveWorkspaceIdsByUserId(userId) {
|
|
145
|
+
assert.equal(userId, "9");
|
|
146
|
+
return ["11", "12"];
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
assert.equal(audienceContributors.length, 1);
|
|
153
|
+
assert.deepEqual(await audienceContributors[0].resolveActorScopes("9"), [
|
|
154
|
+
{ kind: "workspace", id: "11" },
|
|
155
|
+
{ kind: "workspace_user", workspaceId: "11", userId: "9" },
|
|
156
|
+
{ kind: "workspace", id: "12" },
|
|
157
|
+
{ kind: "workspace_user", workspaceId: "12", userId: "9" }
|
|
158
|
+
]);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("workspace settings color defaults must be hex colors", () => {
|
|
162
|
+
const config = runtimeConfig();
|
|
163
|
+
config.workspaceSettings.defaults.lightPrimaryColor = "charcoal";
|
|
164
|
+
const jsonRestApi = createJsonRestApiFixture();
|
|
165
|
+
jsonRestApi.resources.workspaceSettings = {};
|
|
166
|
+
|
|
167
|
+
assert.throws(
|
|
168
|
+
() => createWorkspacesRuntime({ config, env: {}, jsonRestApi }),
|
|
169
|
+
/runtime\.config\.workspaceSettings\.defaults\.lightPrimaryColor must be a hex color/
|
|
170
|
+
);
|
|
120
171
|
});
|
|
@@ -3,7 +3,7 @@ import assert from "node:assert/strict";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { existsSync } from "node:fs";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import {
|
|
6
|
+
import { resolveSchemaTransportSchemaDefinition } from "@jskit-ai/kernel/shared/validators";
|
|
7
7
|
import { workspaceMembersResource } from "../src/shared/resources/workspaceMembersResource.js";
|
|
8
8
|
import { workspacePendingInvitationsResource } from "../src/shared/resources/workspacePendingInvitationsResource.js";
|
|
9
9
|
import { workspaceInvitesResource } from "../src/shared/resources/workspaceInvitesResource.js";
|
|
@@ -87,7 +87,7 @@ test("workspaces-core specialized resource operations expose messages and valida
|
|
|
87
87
|
for (const { label, operation } of operationSpecs) {
|
|
88
88
|
assert.equal(typeof operation?.messages, "object", `${label}.messages must be an object.`);
|
|
89
89
|
assert.equal(
|
|
90
|
-
typeof
|
|
90
|
+
typeof resolveSchemaTransportSchemaDefinition(operation?.output, {
|
|
91
91
|
context: `${label}.output`,
|
|
92
92
|
defaultMode: "replace"
|
|
93
93
|
}),
|
|
@@ -65,3 +65,19 @@ test("resolveTenancyProfile allows explicit workspace self-create policy overrid
|
|
|
65
65
|
assert.equal(workspaceProfile.mode, TENANCY_MODE_WORKSPACES);
|
|
66
66
|
assert.equal(workspaceProfile.workspace.allowSelfCreate, true);
|
|
67
67
|
});
|
|
68
|
+
|
|
69
|
+
test("resolveTenancyProfile lets workspace mode opt into a personal workspace for every user", () => {
|
|
70
|
+
const tenancyPolicy = { workspace: { autoProvision: true } };
|
|
71
|
+
assert.equal(
|
|
72
|
+
resolveTenancyProfile({ tenancyMode: TENANCY_MODE_WORKSPACES, tenancyPolicy }).workspace.autoProvision,
|
|
73
|
+
true
|
|
74
|
+
);
|
|
75
|
+
assert.equal(resolveTenancyProfile({ tenancyMode: TENANCY_MODE_NONE, tenancyPolicy }).workspace.autoProvision, false);
|
|
76
|
+
assert.equal(
|
|
77
|
+
resolveTenancyProfile({
|
|
78
|
+
tenancyMode: TENANCY_MODE_PERSONAL,
|
|
79
|
+
tenancyPolicy: { workspace: { autoProvision: false } }
|
|
80
|
+
}).workspace.autoProvision,
|
|
81
|
+
true
|
|
82
|
+
);
|
|
83
|
+
});
|
|
@@ -4,7 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { existsSync } from "node:fs";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { deriveResourceRequiredMetadata } from "@jskit-ai/kernel/_testable";
|
|
7
|
-
import {
|
|
7
|
+
import { resolveSchemaTransportSchemaDefinition } from "@jskit-ai/kernel/shared/validators";
|
|
8
8
|
import { workspaceMembersResource } from "../src/shared/resources/workspaceMembersResource.js";
|
|
9
9
|
import { workspacePendingInvitationsResource } from "../src/shared/resources/workspacePendingInvitationsResource.js";
|
|
10
10
|
import { workspaceResource } from "../src/shared/resources/workspaceResource.js";
|
|
@@ -29,7 +29,7 @@ function assertResourceShape(resource, label) {
|
|
|
29
29
|
`${label}.operations.${operationName} must resolve messages from operation.messages or resource.messages.`
|
|
30
30
|
);
|
|
31
31
|
assert.equal(
|
|
32
|
-
typeof
|
|
32
|
+
typeof resolveSchemaTransportSchemaDefinition(operation.output, {
|
|
33
33
|
context: `${label}.operations.${operationName}.output`,
|
|
34
34
|
defaultMode: "replace"
|
|
35
35
|
}),
|
|
@@ -76,7 +76,7 @@ test("workspace settings and invite operations expose canonical validators", ()
|
|
|
76
76
|
for (const { label, operation } of operationSpecs) {
|
|
77
77
|
assert.equal(typeof operation?.method, "string", `${label}.method must exist.`);
|
|
78
78
|
assert.equal(
|
|
79
|
-
typeof
|
|
79
|
+
typeof resolveSchemaTransportSchemaDefinition(operation?.output, {
|
|
80
80
|
context: `${label}.output`,
|
|
81
81
|
defaultMode: "replace"
|
|
82
82
|
}),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import { validateOperationSection } from "@jskit-ai/http-runtime/shared/validators/operationValidation";
|
|
4
|
-
import {
|
|
4
|
+
import { resolveSchemaTransportSchemaDefinition } from "@jskit-ai/kernel/shared/validators";
|
|
5
5
|
import { workspaceInvitesResource } from "../src/shared/resources/workspaceInvitesResource.js";
|
|
6
6
|
import { workspaceMembershipsResource } from "../src/shared/resources/workspaceMembershipsResource.js";
|
|
7
7
|
|
|
@@ -24,7 +24,7 @@ test("workspace internal CRUD resources expose canonical derived operation sets"
|
|
|
24
24
|
assert.equal(typeof resource.operations.create.body?.schema, "object", `${label}.operations.create.body.schema is required.`);
|
|
25
25
|
assert.equal(typeof resource.operations.patch.body?.schema, "object", `${label}.operations.patch.body.schema is required.`);
|
|
26
26
|
assert.equal(
|
|
27
|
-
typeof
|
|
27
|
+
typeof resolveSchemaTransportSchemaDefinition(resource.operations.view.output, {
|
|
28
28
|
context: `${label}.operations.view.output`,
|
|
29
29
|
defaultMode: "replace"
|
|
30
30
|
}),
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import {
|
|
3
|
+
import { resolveSchemaTransportSchemaDefinition } from "@jskit-ai/kernel/shared/validators";
|
|
4
4
|
import { workspaceMembersResource } from "../src/shared/resources/workspaceMembersResource.js";
|
|
5
5
|
|
|
6
6
|
function resolveOutputSchema(operationName) {
|
|
7
|
-
return
|
|
7
|
+
return resolveSchemaTransportSchemaDefinition(workspaceMembersResource.operations[operationName].output, {
|
|
8
8
|
context: `workspaceMembers.${operationName}.output`,
|
|
9
9
|
defaultMode: "replace"
|
|
10
10
|
});
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import {
|
|
3
|
+
import { resolveSchemaTransportSchemaDefinition } from "@jskit-ai/kernel/shared/validators";
|
|
4
4
|
import { workspacePendingInvitationsResource } from "../src/shared/resources/workspacePendingInvitationsResource.js";
|
|
5
5
|
|
|
6
6
|
test("workspacePendingInvitationsResource output schema accepts already-shaped invite payloads", () => {
|
|
7
|
-
const outputSchema =
|
|
7
|
+
const outputSchema = resolveSchemaTransportSchemaDefinition(workspacePendingInvitationsResource.operations.list.output, {
|
|
8
8
|
context: "workspacePendingInvitations.list.output",
|
|
9
9
|
defaultMode: "replace"
|
|
10
10
|
});
|
|
@@ -33,11 +33,11 @@ test("workspacePendingInvitationsResource output schema accepts already-shaped i
|
|
|
33
33
|
});
|
|
34
34
|
|
|
35
35
|
test("workspacePendingInvitationsResource exposes public resolve query and terminal statuses", () => {
|
|
36
|
-
const querySchema =
|
|
36
|
+
const querySchema = resolveSchemaTransportSchemaDefinition(workspacePendingInvitationsResource.operations.resolve.query, {
|
|
37
37
|
context: "workspacePendingInvitations.resolve.query",
|
|
38
38
|
defaultMode: "replace"
|
|
39
39
|
});
|
|
40
|
-
const outputSchema =
|
|
40
|
+
const outputSchema = resolveSchemaTransportSchemaDefinition(workspacePendingInvitationsResource.operations.resolve.output, {
|
|
41
41
|
context: "workspacePendingInvitations.resolve.output",
|
|
42
42
|
defaultMode: "replace"
|
|
43
43
|
});
|
|
@@ -27,6 +27,7 @@ function createWorkspaceServiceFixture({
|
|
|
27
27
|
additionalWorkspaces = [],
|
|
28
28
|
userWorkspaceRows = null,
|
|
29
29
|
membershipResolver = null,
|
|
30
|
+
personalWorkspaceName = undefined,
|
|
30
31
|
personalWorkspace = {
|
|
31
32
|
id: "1",
|
|
32
33
|
slug: "tonymobily3",
|
|
@@ -72,6 +73,7 @@ function createWorkspaceServiceFixture({
|
|
|
72
73
|
appConfig: {
|
|
73
74
|
tenancyMode,
|
|
74
75
|
tenancyPolicy,
|
|
76
|
+
personalWorkspaceName,
|
|
75
77
|
roleCatalog: roleCatalog && typeof roleCatalog === "object" ? { ...roleCatalog } : roleCatalog
|
|
76
78
|
},
|
|
77
79
|
workspacesRepository: {
|
|
@@ -278,6 +280,32 @@ test("workspaceService.ensureProvisionedWorkspaceForAuthenticatedUser provisions
|
|
|
278
280
|
assert.equal(calls.ensureOwnerMembership, 1);
|
|
279
281
|
assert.equal(calls.ensureWorkspaceSettings, 1);
|
|
280
282
|
assert.equal(insertedPayloads[0].isPersonal, true);
|
|
283
|
+
assert.equal(insertedPayloads[0].name, "Chiara's Workspace");
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("workspaceService.ensureProvisionedWorkspaceForAuthenticatedUser provisions a named personal workspace when workspace tenancy opts in", async () => {
|
|
287
|
+
const { service, calls, insertedPayloads } = createWorkspaceServiceFixture({
|
|
288
|
+
tenancyMode: "workspaces",
|
|
289
|
+
tenancyPolicy: {
|
|
290
|
+
workspace: {
|
|
291
|
+
autoProvision: true
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
personalWorkspaceName: (user) => `${user.displayName}'s home`,
|
|
295
|
+
personalWorkspace: null
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
const workspace = await service.ensureProvisionedWorkspaceForAuthenticatedUser({
|
|
299
|
+
id: "7",
|
|
300
|
+
email: "chiaramobily@gmail.com",
|
|
301
|
+
displayName: "Chiara"
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
assert.equal(Number(workspace.ownerUserId), 7);
|
|
305
|
+
assert.equal(calls.insert, 1);
|
|
306
|
+
assert.equal(calls.ensureOwnerMembership, 1);
|
|
307
|
+
assert.equal(insertedPayloads[0].isPersonal, true);
|
|
308
|
+
assert.equal(insertedPayloads[0].name, "Chiara's home");
|
|
281
309
|
});
|
|
282
310
|
|
|
283
311
|
test("workspaceService.ensureProvisionedWorkspaceForAuthenticatedUser reuses the existing personal workspace in personal tenancy", async () => {
|
|
@@ -181,6 +181,25 @@ test("workspaceSettingsRepository.ensureForWorkspaceId delegates defaults to the
|
|
|
181
181
|
assert.equal(record.id, "5");
|
|
182
182
|
});
|
|
183
183
|
|
|
184
|
+
test("workspaceSettingsRepository.ensureForWorkspaceId starts new workspaces from the app's default colors", async () => {
|
|
185
|
+
const { api, state, DEFAULT_WORKSPACE_THEME } = createWorkspaceSettingsApiStub();
|
|
186
|
+
state.row = null;
|
|
187
|
+
const repository = createRepository({
|
|
188
|
+
api,
|
|
189
|
+
defaultColorSettings: {
|
|
190
|
+
lightPrimaryColor: "#1C1C1C",
|
|
191
|
+
darkPrimaryColor: "#F5F5F5"
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
await repository.ensureForWorkspaceId("5");
|
|
196
|
+
|
|
197
|
+
assert.equal(state.postPayload.data.lightPrimaryColor, "#1C1C1C");
|
|
198
|
+
assert.equal(state.postPayload.data.darkPrimaryColor, "#F5F5F5");
|
|
199
|
+
assert.equal(state.postPayload.data.lightSecondaryColor, DEFAULT_WORKSPACE_THEME.light.secondaryColor);
|
|
200
|
+
assert.equal(state.postPayload.data.darkSurfaceColor, DEFAULT_WORKSPACE_THEME.dark.surfaceColor);
|
|
201
|
+
});
|
|
202
|
+
|
|
184
203
|
test("workspaceSettingsRepository.updateSettingsByWorkspaceId updates workspace settings fields", async () => {
|
|
185
204
|
const { api, state } = createWorkspaceSettingsApiStub();
|
|
186
205
|
const repository = createRepository({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
import { validateOperationSection } from "@jskit-ai/http-runtime/shared/validators/operationValidation";
|
|
4
|
-
import {
|
|
4
|
+
import { resolveSchemaTransportSchemaDefinition } from "@jskit-ai/kernel/shared/validators";
|
|
5
5
|
import { resolveWorkspaceThemePalettes } from "@jskit-ai/workspaces-core/shared/settings";
|
|
6
6
|
import {
|
|
7
7
|
WORKSPACE_SETTINGS_FIELD_KEYS,
|
|
@@ -102,7 +102,7 @@ test("workspace settings create body requires full-write fields", async () => {
|
|
|
102
102
|
});
|
|
103
103
|
|
|
104
104
|
test("workspace settings output schema accepts already-shaped service payloads", () => {
|
|
105
|
-
const outputSchema =
|
|
105
|
+
const outputSchema = resolveSchemaTransportSchemaDefinition(workspaceSettingsResource.operations.view.output, {
|
|
106
106
|
context: "workspaceSettings.view.output",
|
|
107
107
|
defaultMode: "replace"
|
|
108
108
|
});
|