@jskit-ai/users-core 0.1.21 → 0.1.23
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.descriptor.mjs +14 -5
- package/package.json +6 -5
- package/src/server/common/formatters/workspaceFormatter.js +0 -2
- package/src/server/common/repositories/workspacesRepository.js +7 -11
- package/src/server/common/services/workspaceContextService.js +12 -0
- package/src/server/workspaceDirectory/bootWorkspaceDirectoryRoutes.js +64 -0
- package/src/server/workspaceDirectory/workspaceDirectoryActions.js +68 -0
- package/src/shared/resources/workspaceResource.js +44 -2
- package/templates/migrations/users_core_workspace_settings_single_name_source.cjs +71 -0
- package/templates/packages/main/src/shared/resources/workspaceSettingsFields.js +0 -52
- package/test/registerWorkspaceDirectory.test.js +6 -1
- package/test/usersRouteRequestInputValidator.test.js +73 -3
- package/test/workspaceService.test.js +78 -0
- package/test/workspaceSettingsActions.test.js +10 -0
- package/test/workspaceSettingsRepository.test.js +1 -23
- package/test/workspaceSettingsResource.test.js +6 -27
- package/test/workspaceSettingsService.test.js +0 -11
package/package.descriptor.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export default Object.freeze({
|
|
2
2
|
packageVersion: 1,
|
|
3
3
|
packageId: "@jskit-ai/users-core",
|
|
4
|
-
version: "0.1.
|
|
4
|
+
version: "0.1.23",
|
|
5
5
|
description: "Users/workspace domain runtime plus HTTP routes for workspace, account, and console features.",
|
|
6
6
|
dependsOn: [
|
|
7
7
|
"@jskit-ai/auth-core",
|
|
@@ -203,10 +203,10 @@ export default Object.freeze({
|
|
|
203
203
|
mutations: {
|
|
204
204
|
dependencies: {
|
|
205
205
|
runtime: {
|
|
206
|
-
"@jskit-ai/auth-core": "0.1.
|
|
207
|
-
"@jskit-ai/database-runtime": "0.1.
|
|
208
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
209
|
-
"@jskit-ai/kernel": "0.1.
|
|
206
|
+
"@jskit-ai/auth-core": "0.1.16",
|
|
207
|
+
"@jskit-ai/database-runtime": "0.1.17",
|
|
208
|
+
"@jskit-ai/http-runtime": "0.1.16",
|
|
209
|
+
"@jskit-ai/kernel": "0.1.17",
|
|
210
210
|
"@fastify/multipart": "^9.4.0",
|
|
211
211
|
"@fastify/type-provider-typebox": "^6.1.0",
|
|
212
212
|
"typebox": "^1.0.81"
|
|
@@ -252,6 +252,15 @@ export default Object.freeze({
|
|
|
252
252
|
category: "migration",
|
|
253
253
|
id: "users-core-console-owner-schema"
|
|
254
254
|
},
|
|
255
|
+
{
|
|
256
|
+
op: "install-migration",
|
|
257
|
+
from: "templates/migrations/users_core_workspace_settings_single_name_source.cjs",
|
|
258
|
+
toDir: "migrations",
|
|
259
|
+
extension: ".cjs",
|
|
260
|
+
reason: "Remove workspace_settings name/avatar fields so workspace identity data comes from workspaces only.",
|
|
261
|
+
category: "migration",
|
|
262
|
+
id: "users-core-workspace-settings-single-name-source"
|
|
263
|
+
},
|
|
255
264
|
{
|
|
256
265
|
from: "templates/packages/main/src/shared/resources/workspaceSettingsFields.js",
|
|
257
266
|
to: "packages/main/src/shared/resources/workspaceSettingsFields.js",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/users-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.23",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"./shared/support/usersVisibility": "./src/shared/support/usersVisibility.js",
|
|
17
17
|
"./shared/support/workspacePathModel": "./src/shared/support/workspacePathModel.js",
|
|
18
18
|
"./shared/support/usersApiPaths": "./src/shared/support/usersApiPaths.js",
|
|
19
|
+
"./shared/resources/workspaceResource": "./src/shared/resources/workspaceResource.js",
|
|
19
20
|
"./shared/resources/workspaceSettingsResource": "./src/shared/resources/workspaceSettingsResource.js",
|
|
20
21
|
"./shared/resources/workspaceSettingsFields": "./src/shared/resources/workspaceSettingsFields.js",
|
|
21
22
|
"./shared/resources/userProfileResource": "./src/shared/resources/userProfileResource.js",
|
|
@@ -24,10 +25,10 @@
|
|
|
24
25
|
"./shared/resources/consoleSettingsFields": "./src/shared/resources/consoleSettingsFields.js"
|
|
25
26
|
},
|
|
26
27
|
"dependencies": {
|
|
27
|
-
"@jskit-ai/auth-core": "0.1.
|
|
28
|
-
"@jskit-ai/database-runtime": "0.1.
|
|
29
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
30
|
-
"@jskit-ai/kernel": "0.1.
|
|
28
|
+
"@jskit-ai/auth-core": "0.1.16",
|
|
29
|
+
"@jskit-ai/database-runtime": "0.1.17",
|
|
30
|
+
"@jskit-ai/http-runtime": "0.1.16",
|
|
31
|
+
"@jskit-ai/kernel": "0.1.17",
|
|
31
32
|
"@fastify/multipart": "^9.4.0",
|
|
32
33
|
"@fastify/type-provider-typebox": "^6.1.0",
|
|
33
34
|
"typebox": "^1.0.81"
|
|
@@ -23,7 +23,6 @@ function mapWorkspaceSettingsPublic(workspaceSettings, { workspaceInvitationsEna
|
|
|
23
23
|
const themePalettes = resolveWorkspaceThemePalettes(source);
|
|
24
24
|
|
|
25
25
|
return {
|
|
26
|
-
name: normalizeText(source.name),
|
|
27
26
|
lightPrimaryColor: themePalettes.light.color,
|
|
28
27
|
lightSecondaryColor: themePalettes.light.secondaryColor,
|
|
29
28
|
lightSurfaceColor: themePalettes.light.surfaceColor,
|
|
@@ -32,7 +31,6 @@ function mapWorkspaceSettingsPublic(workspaceSettings, { workspaceInvitationsEna
|
|
|
32
31
|
darkSecondaryColor: themePalettes.dark.secondaryColor,
|
|
33
32
|
darkSurfaceColor: themePalettes.dark.surfaceColor,
|
|
34
33
|
darkSurfaceVariantColor: themePalettes.dark.surfaceVariantColor,
|
|
35
|
-
avatarUrl: normalizeText(source.avatarUrl),
|
|
36
34
|
invitesEnabled,
|
|
37
35
|
invitesAvailable,
|
|
38
36
|
invitesEffective: invitesAvailable && invitesEnabled
|
|
@@ -45,14 +45,14 @@ function createRepository(knex) {
|
|
|
45
45
|
throw new TypeError("workspacesRepository requires knex.");
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
function workspaceSelectColumns(
|
|
48
|
+
function workspaceSelectColumns({ includeMembership = false } = {}) {
|
|
49
49
|
const columns = [
|
|
50
50
|
"w.id",
|
|
51
51
|
"w.slug",
|
|
52
|
-
|
|
52
|
+
"w.name",
|
|
53
53
|
"w.owner_user_id",
|
|
54
54
|
"w.is_personal",
|
|
55
|
-
|
|
55
|
+
"w.avatar_url",
|
|
56
56
|
"w.color",
|
|
57
57
|
"w.created_at",
|
|
58
58
|
"w.updated_at",
|
|
@@ -67,9 +67,8 @@ function createRepository(knex) {
|
|
|
67
67
|
async function findById(workspaceId, options = {}) {
|
|
68
68
|
const client = options?.trx || knex;
|
|
69
69
|
const row = await client("workspaces as w")
|
|
70
|
-
.leftJoin("workspace_settings as ws", "ws.workspace_id", "w.id")
|
|
71
70
|
.where({ "w.id": Number(workspaceId) })
|
|
72
|
-
.select(workspaceSelectColumns(
|
|
71
|
+
.select(workspaceSelectColumns())
|
|
73
72
|
.first();
|
|
74
73
|
return mapRow(row);
|
|
75
74
|
}
|
|
@@ -82,9 +81,8 @@ function createRepository(knex) {
|
|
|
82
81
|
}
|
|
83
82
|
|
|
84
83
|
const row = await client("workspaces as w")
|
|
85
|
-
.leftJoin("workspace_settings as ws", "ws.workspace_id", "w.id")
|
|
86
84
|
.where({ "w.slug": normalizedSlug })
|
|
87
|
-
.select(workspaceSelectColumns(
|
|
85
|
+
.select(workspaceSelectColumns())
|
|
88
86
|
.first();
|
|
89
87
|
return mapRow(row);
|
|
90
88
|
}
|
|
@@ -92,10 +90,9 @@ function createRepository(knex) {
|
|
|
92
90
|
async function findPersonalByOwnerUserId(userId, options = {}) {
|
|
93
91
|
const client = options?.trx || knex;
|
|
94
92
|
const row = await client("workspaces as w")
|
|
95
|
-
.leftJoin("workspace_settings as ws", "ws.workspace_id", "w.id")
|
|
96
93
|
.where({ "w.owner_user_id": Number(userId), "w.is_personal": 1 })
|
|
97
94
|
.orderBy("w.id", "asc")
|
|
98
|
-
.select(workspaceSelectColumns(
|
|
95
|
+
.select(workspaceSelectColumns())
|
|
99
96
|
.first();
|
|
100
97
|
return mapRow(row);
|
|
101
98
|
}
|
|
@@ -161,12 +158,11 @@ function createRepository(knex) {
|
|
|
161
158
|
const client = options?.trx || knex;
|
|
162
159
|
const rows = await client("workspace_memberships as wm")
|
|
163
160
|
.join("workspaces as w", "w.id", "wm.workspace_id")
|
|
164
|
-
.leftJoin("workspace_settings as ws", "ws.workspace_id", "w.id")
|
|
165
161
|
.where({ "wm.user_id": Number(userId) })
|
|
166
162
|
.whereNull("w.deleted_at")
|
|
167
163
|
.orderBy("w.is_personal", "desc")
|
|
168
164
|
.orderBy("w.id", "asc")
|
|
169
|
-
.select(workspaceSelectColumns(
|
|
165
|
+
.select(workspaceSelectColumns({ includeMembership: true }));
|
|
170
166
|
|
|
171
167
|
return rows.map(mapMembershipWorkspaceRow).filter(Boolean);
|
|
172
168
|
}
|
|
@@ -205,6 +205,16 @@ function createService({
|
|
|
205
205
|
return inserted;
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
async function getWorkspaceForAuthenticatedUser(user, workspaceSlug, options = {}) {
|
|
209
|
+
const workspaceContext = await resolveWorkspaceContextForUserBySlug(user, workspaceSlug, options);
|
|
210
|
+
return workspaceContext.workspace;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function updateWorkspaceForAuthenticatedUser(user, workspaceSlug, patch = {}, options = {}) {
|
|
214
|
+
const workspaceContext = await resolveWorkspaceContextForUserBySlug(user, workspaceSlug, options);
|
|
215
|
+
return workspacesRepository.updateById(workspaceContext.workspace.id, patch, options);
|
|
216
|
+
}
|
|
217
|
+
|
|
208
218
|
async function resolveWorkspaceContextForUserBySlug(user, workspaceSlug, options = {}) {
|
|
209
219
|
const normalizedUser = authenticatedUserValidator.normalize(user);
|
|
210
220
|
if (!normalizedUser) {
|
|
@@ -261,6 +271,8 @@ function createService({
|
|
|
261
271
|
ensurePersonalWorkspaceForUser,
|
|
262
272
|
provisionWorkspaceForNewUser,
|
|
263
273
|
createWorkspaceForAuthenticatedUser,
|
|
274
|
+
getWorkspaceForAuthenticatedUser,
|
|
275
|
+
updateWorkspaceForAuthenticatedUser,
|
|
264
276
|
listWorkspacesForUser,
|
|
265
277
|
listWorkspacesForAuthenticatedUser,
|
|
266
278
|
resolveWorkspaceContextForUserBySlug
|
|
@@ -4,6 +4,9 @@ import { workspaceResource } from "../../shared/resources/workspaceResource.js";
|
|
|
4
4
|
import {
|
|
5
5
|
USERS_WORKSPACE_SELF_CREATE_ENABLED_TOKEN
|
|
6
6
|
} from "../common/diTokens.js";
|
|
7
|
+
import { resolveWorkspaceRoutePath } from "../common/support/workspaceRoutePaths.js";
|
|
8
|
+
import { workspaceSlugParamsValidator } from "../common/validators/routeParamsValidator.js";
|
|
9
|
+
import { resolveDefaultWorkspaceRouteSurfaceIdFromAppConfig } from "../support/workspaceActionSurfaces.js";
|
|
7
10
|
|
|
8
11
|
function bootWorkspaceDirectoryRoutes(app) {
|
|
9
12
|
if (!app || typeof app.make !== "function" || typeof app.has !== "function") {
|
|
@@ -11,6 +14,8 @@ function bootWorkspaceDirectoryRoutes(app) {
|
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
const router = app.make(KERNEL_TOKENS.HttpRouter);
|
|
17
|
+
const appConfig = app.has("appConfig") ? app.make("appConfig") : {};
|
|
18
|
+
const workspaceRouteSurfaceId = resolveDefaultWorkspaceRouteSurfaceIdFromAppConfig(appConfig);
|
|
14
19
|
const workspaceSelfCreateEnabled = app.has(USERS_WORKSPACE_SELF_CREATE_ENABLED_TOKEN)
|
|
15
20
|
? app.make(USERS_WORKSPACE_SELF_CREATE_ENABLED_TOKEN) === true
|
|
16
21
|
: false;
|
|
@@ -68,6 +73,65 @@ function bootWorkspaceDirectoryRoutes(app) {
|
|
|
68
73
|
reply.code(200).send(response);
|
|
69
74
|
}
|
|
70
75
|
);
|
|
76
|
+
|
|
77
|
+
router.register(
|
|
78
|
+
"GET",
|
|
79
|
+
resolveWorkspaceRoutePath("/"),
|
|
80
|
+
{
|
|
81
|
+
auth: "required",
|
|
82
|
+
surface: workspaceRouteSurfaceId,
|
|
83
|
+
visibility: "workspace",
|
|
84
|
+
meta: {
|
|
85
|
+
tags: ["workspace"],
|
|
86
|
+
summary: "Get workspace profile by workspace slug"
|
|
87
|
+
},
|
|
88
|
+
paramsValidator: workspaceSlugParamsValidator,
|
|
89
|
+
responseValidators: withStandardErrorResponses({
|
|
90
|
+
200: workspaceResource.operations.view.outputValidator
|
|
91
|
+
})
|
|
92
|
+
},
|
|
93
|
+
async function (request, reply) {
|
|
94
|
+
const response = await request.executeAction({
|
|
95
|
+
actionId: "workspace.workspaces.read",
|
|
96
|
+
input: {
|
|
97
|
+
workspaceSlug: request.input.params.workspaceSlug
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
reply.code(200).send(response);
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
router.register(
|
|
105
|
+
"PATCH",
|
|
106
|
+
resolveWorkspaceRoutePath("/"),
|
|
107
|
+
{
|
|
108
|
+
auth: "required",
|
|
109
|
+
surface: workspaceRouteSurfaceId,
|
|
110
|
+
visibility: "workspace",
|
|
111
|
+
meta: {
|
|
112
|
+
tags: ["workspace"],
|
|
113
|
+
summary: "Update workspace profile by workspace slug"
|
|
114
|
+
},
|
|
115
|
+
paramsValidator: workspaceSlugParamsValidator,
|
|
116
|
+
bodyValidator: workspaceResource.operations.patch.bodyValidator,
|
|
117
|
+
responseValidators: withStandardErrorResponses(
|
|
118
|
+
{
|
|
119
|
+
200: workspaceResource.operations.patch.outputValidator
|
|
120
|
+
},
|
|
121
|
+
{ includeValidation400: true }
|
|
122
|
+
)
|
|
123
|
+
},
|
|
124
|
+
async function (request, reply) {
|
|
125
|
+
const response = await request.executeAction({
|
|
126
|
+
actionId: "workspace.workspaces.update",
|
|
127
|
+
input: {
|
|
128
|
+
workspaceSlug: request.input.params.workspaceSlug,
|
|
129
|
+
patch: request.input.body
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
reply.code(200).send(response);
|
|
133
|
+
}
|
|
134
|
+
);
|
|
71
135
|
}
|
|
72
136
|
|
|
73
137
|
export { bootWorkspaceDirectoryRoutes };
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
resolveRequest
|
|
4
4
|
} from "@jskit-ai/kernel/shared/actions/actionContributorHelpers";
|
|
5
5
|
import { workspaceResource } from "../../shared/resources/workspaceResource.js";
|
|
6
|
+
import { workspaceSlugParamsValidator } from "../common/validators/routeParamsValidator.js";
|
|
6
7
|
import { resolveActionUser } from "../common/support/resolveActionUser.js";
|
|
7
8
|
|
|
8
9
|
const workspaceDirectoryActions = Object.freeze([
|
|
@@ -59,6 +60,73 @@ const workspaceDirectoryActions = Object.freeze([
|
|
|
59
60
|
nextCursor: null
|
|
60
61
|
};
|
|
61
62
|
}
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: "workspace.workspaces.read",
|
|
66
|
+
version: 1,
|
|
67
|
+
kind: "query",
|
|
68
|
+
channels: ["api", "automation", "internal"],
|
|
69
|
+
surfacesFrom: "workspace",
|
|
70
|
+
permission: {
|
|
71
|
+
require: "any",
|
|
72
|
+
permissions: ["workspace.settings.view", "workspace.settings.update"]
|
|
73
|
+
},
|
|
74
|
+
inputValidator: workspaceSlugParamsValidator,
|
|
75
|
+
outputValidator: workspaceResource.operations.view.outputValidator,
|
|
76
|
+
idempotency: "none",
|
|
77
|
+
audit: {
|
|
78
|
+
actionName: "workspace.workspaces.read"
|
|
79
|
+
},
|
|
80
|
+
observability: {},
|
|
81
|
+
async execute(input, context, deps) {
|
|
82
|
+
return deps.workspaceService.getWorkspaceForAuthenticatedUser(
|
|
83
|
+
resolveActionUser(context, input),
|
|
84
|
+
input.workspaceSlug,
|
|
85
|
+
{
|
|
86
|
+
request: resolveRequest(context),
|
|
87
|
+
context
|
|
88
|
+
}
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: "workspace.workspaces.update",
|
|
94
|
+
version: 1,
|
|
95
|
+
kind: "command",
|
|
96
|
+
channels: ["api", "assistant_tool", "automation", "internal"],
|
|
97
|
+
surfacesFrom: "workspace",
|
|
98
|
+
permission: {
|
|
99
|
+
require: "all",
|
|
100
|
+
permissions: ["workspace.settings.update"]
|
|
101
|
+
},
|
|
102
|
+
inputValidator: [
|
|
103
|
+
workspaceSlugParamsValidator,
|
|
104
|
+
{
|
|
105
|
+
patch: workspaceResource.operations.patch.bodyValidator
|
|
106
|
+
}
|
|
107
|
+
],
|
|
108
|
+
outputValidator: workspaceResource.operations.patch.outputValidator,
|
|
109
|
+
idempotency: "optional",
|
|
110
|
+
audit: {
|
|
111
|
+
actionName: "workspace.workspaces.update"
|
|
112
|
+
},
|
|
113
|
+
observability: {},
|
|
114
|
+
extensions: {
|
|
115
|
+
assistant: {
|
|
116
|
+
description: "Update workspace profile fields."
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
async execute(input, context, deps) {
|
|
120
|
+
return deps.workspaceService.updateWorkspaceForAuthenticatedUser(
|
|
121
|
+
resolveActionUser(context, input),
|
|
122
|
+
input.workspaceSlug,
|
|
123
|
+
input.patch,
|
|
124
|
+
{
|
|
125
|
+
request: resolveRequest(context),
|
|
126
|
+
context
|
|
127
|
+
}
|
|
128
|
+
);
|
|
129
|
+
}
|
|
62
130
|
}
|
|
63
131
|
]);
|
|
64
132
|
|
|
@@ -5,6 +5,21 @@ import {
|
|
|
5
5
|
createCursorListValidator
|
|
6
6
|
} from "@jskit-ai/kernel/shared/validators";
|
|
7
7
|
|
|
8
|
+
function normalizeWorkspaceAvatarUrl(value) {
|
|
9
|
+
const avatarUrl = normalizeText(value);
|
|
10
|
+
if (!avatarUrl) {
|
|
11
|
+
return "";
|
|
12
|
+
}
|
|
13
|
+
if (!avatarUrl.startsWith("http://") && !avatarUrl.startsWith("https://")) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
return new URL(avatarUrl).toString();
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
8
23
|
function normalizeWorkspaceInput(payload = {}) {
|
|
9
24
|
const source = normalizeObjectInput(payload);
|
|
10
25
|
const normalized = {};
|
|
@@ -19,7 +34,7 @@ function normalizeWorkspaceInput(payload = {}) {
|
|
|
19
34
|
normalized.ownerUserId = Number(source.ownerUserId);
|
|
20
35
|
}
|
|
21
36
|
if (Object.hasOwn(source, "avatarUrl")) {
|
|
22
|
-
normalized.avatarUrl =
|
|
37
|
+
normalized.avatarUrl = normalizeWorkspaceAvatarUrl(source.avatarUrl);
|
|
23
38
|
}
|
|
24
39
|
if (Object.hasOwn(source, "color")) {
|
|
25
40
|
const color = normalizeText(source.color);
|
|
@@ -92,6 +107,33 @@ const createRequestBodySchema = Type.Object(
|
|
|
92
107
|
{ additionalProperties: false }
|
|
93
108
|
);
|
|
94
109
|
|
|
110
|
+
const patchRequestBodySchema = Type.Object(
|
|
111
|
+
{
|
|
112
|
+
name: Type.Optional(Type.String({ minLength: 1, maxLength: 160 })),
|
|
113
|
+
avatarUrl: Type.Optional(
|
|
114
|
+
Type.String({
|
|
115
|
+
pattern: "^(https?://.+)?$",
|
|
116
|
+
messages: {
|
|
117
|
+
pattern: "Workspace avatar URL must be a valid absolute URL (http:// or https://).",
|
|
118
|
+
default: "Workspace avatar URL must be a valid absolute URL (http:// or https://)."
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
),
|
|
122
|
+
color: Type.Optional(
|
|
123
|
+
Type.String({
|
|
124
|
+
minLength: 7,
|
|
125
|
+
maxLength: 7,
|
|
126
|
+
pattern: "^#[0-9A-Fa-f]{6}$",
|
|
127
|
+
messages: {
|
|
128
|
+
pattern: "Workspace color must be a hex color like #1867C0.",
|
|
129
|
+
default: "Workspace color must be a hex color like #1867C0."
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
)
|
|
133
|
+
},
|
|
134
|
+
{ additionalProperties: false }
|
|
135
|
+
);
|
|
136
|
+
|
|
95
137
|
const responseRecordValidator = Object.freeze({
|
|
96
138
|
schema: responseRecordSchema,
|
|
97
139
|
normalize: normalizeWorkspaceOutput
|
|
@@ -138,7 +180,7 @@ const resource = {
|
|
|
138
180
|
patch: {
|
|
139
181
|
method: "PATCH",
|
|
140
182
|
bodyValidator: {
|
|
141
|
-
schema:
|
|
183
|
+
schema: patchRequestBodySchema,
|
|
142
184
|
normalize: normalizeWorkspaceInput
|
|
143
185
|
},
|
|
144
186
|
outputValidator: responseRecordValidator
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const WORKSPACE_SETTINGS_TABLE = "workspace_settings";
|
|
2
|
+
const WORKSPACES_TABLE = "workspaces";
|
|
3
|
+
const LEGACY_NAME_COLUMN = "name";
|
|
4
|
+
const LEGACY_AVATAR_COLUMN = "avatar_url";
|
|
5
|
+
|
|
6
|
+
async function hasTable(knex, tableName) {
|
|
7
|
+
return knex.schema.hasTable(tableName);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function hasColumn(knex, tableName, columnName) {
|
|
11
|
+
return knex.schema.hasColumn(tableName, columnName);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
exports.up = async function up(knex) {
|
|
15
|
+
const hasWorkspaceSettings = await hasTable(knex, WORKSPACE_SETTINGS_TABLE);
|
|
16
|
+
if (!hasWorkspaceSettings) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const hasLegacyName = await hasColumn(knex, WORKSPACE_SETTINGS_TABLE, LEGACY_NAME_COLUMN);
|
|
21
|
+
const hasLegacyAvatarUrl = await hasColumn(knex, WORKSPACE_SETTINGS_TABLE, LEGACY_AVATAR_COLUMN);
|
|
22
|
+
if (!hasLegacyName && !hasLegacyAvatarUrl) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
await knex.schema.alterTable(WORKSPACE_SETTINGS_TABLE, (table) => {
|
|
27
|
+
if (hasLegacyName) {
|
|
28
|
+
table.dropColumn(LEGACY_NAME_COLUMN);
|
|
29
|
+
}
|
|
30
|
+
if (hasLegacyAvatarUrl) {
|
|
31
|
+
table.dropColumn(LEGACY_AVATAR_COLUMN);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
exports.down = async function down(knex) {
|
|
37
|
+
const hasWorkspaceSettings = await hasTable(knex, WORKSPACE_SETTINGS_TABLE);
|
|
38
|
+
if (!hasWorkspaceSettings) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const hasLegacyName = await hasColumn(knex, WORKSPACE_SETTINGS_TABLE, LEGACY_NAME_COLUMN);
|
|
43
|
+
const hasLegacyAvatarUrl = await hasColumn(knex, WORKSPACE_SETTINGS_TABLE, LEGACY_AVATAR_COLUMN);
|
|
44
|
+
if (!hasLegacyName || !hasLegacyAvatarUrl) {
|
|
45
|
+
await knex.schema.alterTable(WORKSPACE_SETTINGS_TABLE, (table) => {
|
|
46
|
+
if (!hasLegacyName) {
|
|
47
|
+
table.string(LEGACY_NAME_COLUMN, 160).notNullable().defaultTo("Workspace");
|
|
48
|
+
}
|
|
49
|
+
if (!hasLegacyAvatarUrl) {
|
|
50
|
+
table.string(LEGACY_AVATAR_COLUMN, 512).notNullable().defaultTo("");
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const hasWorkspaces = await hasTable(knex, WORKSPACES_TABLE);
|
|
56
|
+
if (!hasWorkspaces) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const workspaceRows = await knex(WORKSPACES_TABLE).select("id", "name", "avatar_url");
|
|
61
|
+
for (const workspaceRow of workspaceRows) {
|
|
62
|
+
const normalizedName = String(workspaceRow?.name || "").trim() || "Workspace";
|
|
63
|
+
const normalizedAvatarUrl = String(workspaceRow?.avatar_url || "").trim();
|
|
64
|
+
await knex(WORKSPACE_SETTINGS_TABLE)
|
|
65
|
+
.where({ workspace_id: Number(workspaceRow.id) })
|
|
66
|
+
.update({
|
|
67
|
+
name: normalizedName,
|
|
68
|
+
avatar_url: normalizedAvatarUrl
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
};
|
|
@@ -13,21 +13,6 @@ import {
|
|
|
13
13
|
resetWorkspaceSettingsFields
|
|
14
14
|
} from "@jskit-ai/users-core/shared/resources/workspaceSettingsFields";
|
|
15
15
|
|
|
16
|
-
function normalizeAvatarUrl(value) {
|
|
17
|
-
const avatarUrl = normalizeText(value);
|
|
18
|
-
if (!avatarUrl) {
|
|
19
|
-
return "";
|
|
20
|
-
}
|
|
21
|
-
if (!avatarUrl.startsWith("http://") && !avatarUrl.startsWith("https://")) {
|
|
22
|
-
return null;
|
|
23
|
-
}
|
|
24
|
-
try {
|
|
25
|
-
return new URL(avatarUrl).toString();
|
|
26
|
-
} catch {
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
16
|
function normalizeHexColor(value) {
|
|
32
17
|
const color = normalizeText(value);
|
|
33
18
|
return /^#[0-9A-Fa-f]{6}$/.test(color) ? color.toUpperCase() : null;
|
|
@@ -35,43 +20,6 @@ function normalizeHexColor(value) {
|
|
|
35
20
|
|
|
36
21
|
resetWorkspaceSettingsFields();
|
|
37
22
|
|
|
38
|
-
defineField({
|
|
39
|
-
key: "name",
|
|
40
|
-
dbColumn: "name",
|
|
41
|
-
required: true,
|
|
42
|
-
inputSchema: Type.String({
|
|
43
|
-
minLength: 1,
|
|
44
|
-
maxLength: 160,
|
|
45
|
-
messages: {
|
|
46
|
-
required: "Workspace name is required.",
|
|
47
|
-
minLength: "Workspace name is required.",
|
|
48
|
-
maxLength: "Workspace name must be at most 160 characters.",
|
|
49
|
-
default: "Workspace name is required."
|
|
50
|
-
}
|
|
51
|
-
}),
|
|
52
|
-
outputSchema: Type.String({ minLength: 1, maxLength: 160 }),
|
|
53
|
-
normalizeInput: (value) => normalizeText(value),
|
|
54
|
-
normalizeOutput: (value) => normalizeText(value),
|
|
55
|
-
resolveDefault: ({ workspace = {} } = {}) => normalizeText(workspace.name) || "Workspace"
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
defineField({
|
|
59
|
-
key: "avatarUrl",
|
|
60
|
-
dbColumn: "avatar_url",
|
|
61
|
-
required: false,
|
|
62
|
-
inputSchema: Type.String({
|
|
63
|
-
pattern: "^(https?://.+)?$",
|
|
64
|
-
messages: {
|
|
65
|
-
pattern: "Workspace avatar URL must be a valid absolute URL (http:// or https://).",
|
|
66
|
-
default: "Workspace avatar URL must be a valid absolute URL (http:// or https://)."
|
|
67
|
-
}
|
|
68
|
-
}),
|
|
69
|
-
outputSchema: Type.String(),
|
|
70
|
-
normalizeInput: normalizeAvatarUrl,
|
|
71
|
-
normalizeOutput: (value) => normalizeText(value),
|
|
72
|
-
resolveDefault: ({ workspace = {} } = {}) => normalizeText(workspace.avatarUrl)
|
|
73
|
-
});
|
|
74
|
-
|
|
75
23
|
defineField({
|
|
76
24
|
key: "lightPrimaryColor",
|
|
77
25
|
dbColumn: "light_primary_color",
|
|
@@ -22,5 +22,10 @@ test("registerWorkspaceDirectory registers workspace directory actions without r
|
|
|
22
22
|
const app = createAppDouble();
|
|
23
23
|
|
|
24
24
|
registerWorkspaceDirectory(app);
|
|
25
|
-
assert.deepEqual(listActionIds(app), [
|
|
25
|
+
assert.deepEqual(listActionIds(app), [
|
|
26
|
+
"workspace.workspaces.create",
|
|
27
|
+
"workspace.workspaces.list",
|
|
28
|
+
"workspace.workspaces.read",
|
|
29
|
+
"workspace.workspaces.update"
|
|
30
|
+
]);
|
|
26
31
|
});
|
|
@@ -126,6 +126,10 @@ test("workspace and settings routes attach only the shared transport normalizers
|
|
|
126
126
|
method: "GET",
|
|
127
127
|
path: "/api/w/:workspaceSlug/workspace/settings"
|
|
128
128
|
});
|
|
129
|
+
const workspacePatch = findRoute(routes, {
|
|
130
|
+
method: "PATCH",
|
|
131
|
+
path: "/api/w/:workspaceSlug/workspace"
|
|
132
|
+
});
|
|
129
133
|
const workspaceSettingsPatch = findRoute(routes, {
|
|
130
134
|
method: "PATCH",
|
|
131
135
|
path: "/api/w/:workspaceSlug/workspace/settings"
|
|
@@ -156,6 +160,7 @@ test("workspace and settings routes attach only the shared transport normalizers
|
|
|
156
160
|
});
|
|
157
161
|
|
|
158
162
|
assert.equal(typeof workspaceSettings?.paramsValidator?.normalize, "function");
|
|
163
|
+
assert.equal(typeof workspacePatch?.bodyValidator?.normalize, "function");
|
|
159
164
|
assert.equal(typeof workspaceSettingsPatch?.bodyValidator?.normalize, "function");
|
|
160
165
|
assert.equal(typeof workspaceMemberRole?.paramsValidator?.normalize, "function");
|
|
161
166
|
assert.equal(typeof workspaceMemberRole?.bodyValidator?.normalize, "function");
|
|
@@ -167,8 +172,16 @@ test("workspace and settings routes attach only the shared transport normalizers
|
|
|
167
172
|
assert.equal(typeof consoleSettingsPatch?.bodyValidator?.normalize, "function");
|
|
168
173
|
});
|
|
169
174
|
|
|
170
|
-
test("workspace settings routes mount one canonical workspace endpoint", async () => {
|
|
175
|
+
test("workspace core/settings routes mount one canonical workspace endpoint", async () => {
|
|
171
176
|
const routes = await registerRoutes();
|
|
177
|
+
const workspace = findRoute(routes, {
|
|
178
|
+
method: "GET",
|
|
179
|
+
path: "/api/w/:workspaceSlug/workspace"
|
|
180
|
+
});
|
|
181
|
+
const workspacePatch = findRoute(routes, {
|
|
182
|
+
method: "PATCH",
|
|
183
|
+
path: "/api/w/:workspaceSlug/workspace"
|
|
184
|
+
});
|
|
172
185
|
const workspaceSettings = findRoute(routes, {
|
|
173
186
|
method: "GET",
|
|
174
187
|
path: "/api/w/:workspaceSlug/workspace/settings"
|
|
@@ -186,6 +199,11 @@ test("workspace settings routes mount one canonical workspace endpoint", async (
|
|
|
186
199
|
path: "/api/console/w/:workspaceSlug/workspace/settings"
|
|
187
200
|
});
|
|
188
201
|
|
|
202
|
+
assert.ok(workspace);
|
|
203
|
+
assert.equal(workspace?.visibility, "workspace");
|
|
204
|
+
assert.equal(workspacePatch?.visibility, "workspace");
|
|
205
|
+
assert.equal(workspace?.surface, "");
|
|
206
|
+
assert.equal(workspacePatch?.surface, "");
|
|
189
207
|
assert.ok(workspaceSettings);
|
|
190
208
|
assert.equal(workspaceSettings?.visibility, "workspace");
|
|
191
209
|
assert.equal(workspaceSettingsPatch?.visibility, "workspace");
|
|
@@ -205,6 +223,8 @@ test("users-core boot skips workspace routes when workspace policy is disabled",
|
|
|
205
223
|
|
|
206
224
|
assert.equal(findRoute(routes, { method: "GET", path: "/api/workspaces" }), null);
|
|
207
225
|
assert.equal(findRoute(routes, { method: "POST", path: "/api/workspaces" }), null);
|
|
226
|
+
assert.equal(findRoute(routes, { method: "GET", path: "/api/w/:workspaceSlug/workspace" }), null);
|
|
227
|
+
assert.equal(findRoute(routes, { method: "PATCH", path: "/api/w/:workspaceSlug/workspace" }), null);
|
|
208
228
|
assert.equal(findRoute(routes, { method: "GET", path: "/api/w/:workspaceSlug/workspace/settings" }), null);
|
|
209
229
|
assert.equal(findRoute(routes, { method: "GET", path: "/api/settings" })?.path, "/api/settings");
|
|
210
230
|
});
|
|
@@ -242,11 +262,21 @@ test("users-core route registration follows tenancy mode matrix", async () => {
|
|
|
242
262
|
|
|
243
263
|
assert.equal(findRoute(noneRoutes, { method: "GET", path: "/api/workspaces" }), null);
|
|
244
264
|
assert.equal(findRoute(noneRoutes, { method: "POST", path: "/api/workspaces" }), null);
|
|
265
|
+
assert.equal(findRoute(noneRoutes, { method: "GET", path: "/api/w/:workspaceSlug/workspace" }), null);
|
|
266
|
+
assert.equal(findRoute(noneRoutes, { method: "PATCH", path: "/api/w/:workspaceSlug/workspace" }), null);
|
|
245
267
|
assert.equal(findRoute(noneRoutes, { method: "GET", path: "/api/w/:workspaceSlug/workspace/settings" }), null);
|
|
246
268
|
assert.equal(findRoute(noneRoutes, { method: "GET", path: "/api/workspace/invitations/pending" }), null);
|
|
247
269
|
|
|
248
270
|
assert.equal(findRoute(personalRoutes, { method: "GET", path: "/api/workspaces" })?.path, "/api/workspaces");
|
|
249
271
|
assert.equal(findRoute(personalRoutes, { method: "POST", path: "/api/workspaces" }), null);
|
|
272
|
+
assert.equal(
|
|
273
|
+
findRoute(personalRoutes, { method: "GET", path: "/api/w/:workspaceSlug/workspace" })?.path,
|
|
274
|
+
"/api/w/:workspaceSlug/workspace"
|
|
275
|
+
);
|
|
276
|
+
assert.equal(
|
|
277
|
+
findRoute(personalRoutes, { method: "PATCH", path: "/api/w/:workspaceSlug/workspace" })?.path,
|
|
278
|
+
"/api/w/:workspaceSlug/workspace"
|
|
279
|
+
);
|
|
250
280
|
assert.equal(
|
|
251
281
|
findRoute(personalRoutes, { method: "GET", path: "/api/w/:workspaceSlug/workspace/settings" })?.path,
|
|
252
282
|
"/api/w/:workspaceSlug/workspace/settings"
|
|
@@ -258,6 +288,14 @@ test("users-core route registration follows tenancy mode matrix", async () => {
|
|
|
258
288
|
|
|
259
289
|
assert.equal(findRoute(workspaceRoutes, { method: "GET", path: "/api/workspaces" })?.path, "/api/workspaces");
|
|
260
290
|
assert.equal(findRoute(workspaceRoutes, { method: "POST", path: "/api/workspaces" }), null);
|
|
291
|
+
assert.equal(
|
|
292
|
+
findRoute(workspaceRoutes, { method: "GET", path: "/api/w/:workspaceSlug/workspace" })?.path,
|
|
293
|
+
"/api/w/:workspaceSlug/workspace"
|
|
294
|
+
);
|
|
295
|
+
assert.equal(
|
|
296
|
+
findRoute(workspaceRoutes, { method: "PATCH", path: "/api/w/:workspaceSlug/workspace" })?.path,
|
|
297
|
+
"/api/w/:workspaceSlug/workspace"
|
|
298
|
+
);
|
|
261
299
|
assert.equal(
|
|
262
300
|
findRoute(workspaceRoutes, { method: "GET", path: "/api/w/:workspaceSlug/workspace/settings" })?.path,
|
|
263
301
|
"/api/w/:workspaceSlug/workspace/settings"
|
|
@@ -404,7 +442,7 @@ test("workspace settings route handlers build action input from request.input",
|
|
|
404
442
|
createActionRequest({
|
|
405
443
|
input: {
|
|
406
444
|
params: { workspaceSlug: "acme" },
|
|
407
|
-
body: {
|
|
445
|
+
body: { lightPrimaryColor: "#0F6B54" }
|
|
408
446
|
},
|
|
409
447
|
executeAction
|
|
410
448
|
}),
|
|
@@ -413,7 +451,39 @@ test("workspace settings route handlers build action input from request.input",
|
|
|
413
451
|
|
|
414
452
|
assert.deepEqual(calls[0], {
|
|
415
453
|
actionId: "workspace.settings.update",
|
|
416
|
-
input: { workspaceSlug: "acme", patch: {
|
|
454
|
+
input: { workspaceSlug: "acme", patch: { lightPrimaryColor: "#0F6B54" } }
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
test("workspace route handlers build action input from request.input", async () => {
|
|
459
|
+
const routes = await registerRoutes();
|
|
460
|
+
const workspacePatch = findRoute(routes, {
|
|
461
|
+
method: "PATCH",
|
|
462
|
+
path: "/api/w/:workspaceSlug/workspace"
|
|
463
|
+
});
|
|
464
|
+
const calls = [];
|
|
465
|
+
const executeAction = async (payload) => {
|
|
466
|
+
calls.push(payload);
|
|
467
|
+
return {};
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
await workspacePatch.handler(
|
|
471
|
+
createActionRequest({
|
|
472
|
+
input: {
|
|
473
|
+
params: { workspaceSlug: "acme" },
|
|
474
|
+
body: { name: "Acme", avatarUrl: "https://example.com/acme.png", color: "#0F6B54" }
|
|
475
|
+
},
|
|
476
|
+
executeAction
|
|
477
|
+
}),
|
|
478
|
+
createReplyDouble()
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
assert.deepEqual(calls[0], {
|
|
482
|
+
actionId: "workspace.workspaces.update",
|
|
483
|
+
input: {
|
|
484
|
+
workspaceSlug: "acme",
|
|
485
|
+
patch: { name: "Acme", avatarUrl: "https://example.com/acme.png", color: "#0F6B54" }
|
|
486
|
+
}
|
|
417
487
|
});
|
|
418
488
|
});
|
|
419
489
|
|
|
@@ -39,6 +39,7 @@ function createWorkspaceServiceFixture({
|
|
|
39
39
|
findPersonalByOwnerUserId: 0,
|
|
40
40
|
listForUserId: 0,
|
|
41
41
|
insert: 0,
|
|
42
|
+
updateById: 0,
|
|
42
43
|
ensureOwnerMembership: 0
|
|
43
44
|
};
|
|
44
45
|
let nextWorkspaceId = 10;
|
|
@@ -125,6 +126,32 @@ function createWorkspaceServiceFixture({
|
|
|
125
126
|
};
|
|
126
127
|
workspaceBySlug.set(String(inserted.slug).trim().toLowerCase(), inserted);
|
|
127
128
|
return inserted;
|
|
129
|
+
},
|
|
130
|
+
async updateById(workspaceId, patch) {
|
|
131
|
+
calls.updateById += 1;
|
|
132
|
+
const targetId = Number(workspaceId);
|
|
133
|
+
for (const [slug, workspace] of workspaceBySlug.entries()) {
|
|
134
|
+
if (Number(workspace.id) !== targetId) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const updated = {
|
|
138
|
+
...workspace
|
|
139
|
+
};
|
|
140
|
+
if (Object.hasOwn(patch, "name")) {
|
|
141
|
+
updated.name = String(patch.name || "");
|
|
142
|
+
}
|
|
143
|
+
if (Object.hasOwn(patch, "avatarUrl")) {
|
|
144
|
+
updated.avatarUrl = String(patch.avatarUrl || "");
|
|
145
|
+
}
|
|
146
|
+
if (Object.hasOwn(patch, "color")) {
|
|
147
|
+
updated.color = String(patch.color || "#0F6B54");
|
|
148
|
+
}
|
|
149
|
+
workspaceBySlug.set(slug, updated);
|
|
150
|
+
return {
|
|
151
|
+
...updated
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
128
155
|
}
|
|
129
156
|
},
|
|
130
157
|
workspaceMembershipsRepository: {
|
|
@@ -478,3 +505,54 @@ test("workspaceService.resolveWorkspaceContextForUserBySlug resolves permissions
|
|
|
478
505
|
|
|
479
506
|
assert.deepEqual(context.permissions, ["workspace.settings.update"]);
|
|
480
507
|
});
|
|
508
|
+
|
|
509
|
+
test("workspaceService.getWorkspaceForAuthenticatedUser resolves workspace from slug context", async () => {
|
|
510
|
+
const { service } = createWorkspaceServiceFixture({
|
|
511
|
+
additionalWorkspaces: [
|
|
512
|
+
{
|
|
513
|
+
id: 42,
|
|
514
|
+
slug: "team-alpha",
|
|
515
|
+
name: "Team Alpha",
|
|
516
|
+
ownerUserId: 99,
|
|
517
|
+
isPersonal: false,
|
|
518
|
+
avatarUrl: "",
|
|
519
|
+
color: "#0F6B54"
|
|
520
|
+
}
|
|
521
|
+
]
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
const workspace = await service.getWorkspaceForAuthenticatedUser(
|
|
525
|
+
{
|
|
526
|
+
id: 7,
|
|
527
|
+
email: "chiaramobily@gmail.com",
|
|
528
|
+
displayName: "Chiara"
|
|
529
|
+
},
|
|
530
|
+
"team-alpha"
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
assert.equal(workspace.slug, "team-alpha");
|
|
534
|
+
assert.equal(workspace.name, "Team Alpha");
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
test("workspaceService.updateWorkspaceForAuthenticatedUser updates workspace profile fields", async () => {
|
|
538
|
+
const { service, calls } = createWorkspaceServiceFixture();
|
|
539
|
+
|
|
540
|
+
const workspace = await service.updateWorkspaceForAuthenticatedUser(
|
|
541
|
+
{
|
|
542
|
+
id: 7,
|
|
543
|
+
email: "chiaramobily@gmail.com",
|
|
544
|
+
displayName: "Chiara"
|
|
545
|
+
},
|
|
546
|
+
"tonymobily3",
|
|
547
|
+
{
|
|
548
|
+
name: "Updated Workspace",
|
|
549
|
+
avatarUrl: "https://example.com/acme.png",
|
|
550
|
+
color: "#123ABC"
|
|
551
|
+
}
|
|
552
|
+
);
|
|
553
|
+
|
|
554
|
+
assert.equal(calls.updateById, 1);
|
|
555
|
+
assert.equal(workspace.name, "Updated Workspace");
|
|
556
|
+
assert.equal(workspace.avatarUrl, "https://example.com/acme.png");
|
|
557
|
+
assert.equal(workspace.color, "#123ABC");
|
|
558
|
+
});
|
|
@@ -40,3 +40,13 @@ test("workspace directory actions use the canonical workspace list resource outp
|
|
|
40
40
|
assert.ok(listAction);
|
|
41
41
|
assert.equal(listAction.outputValidator, workspaceResource.operations.list.outputValidator);
|
|
42
42
|
});
|
|
43
|
+
|
|
44
|
+
test("workspace directory read/update actions use canonical workspace resource validators", () => {
|
|
45
|
+
const readAction = workspaceDirectoryActions.find((action) => action.id === "workspace.workspaces.read");
|
|
46
|
+
const updateAction = workspaceDirectoryActions.find((action) => action.id === "workspace.workspaces.update");
|
|
47
|
+
|
|
48
|
+
assert.ok(readAction);
|
|
49
|
+
assert.ok(updateAction);
|
|
50
|
+
assert.equal(readAction.outputValidator, workspaceResource.operations.view.outputValidator);
|
|
51
|
+
assert.equal(updateAction.outputValidator, workspaceResource.operations.patch.outputValidator);
|
|
52
|
+
});
|
|
@@ -16,8 +16,6 @@ function createKnexStub(rowOverrides = {}) {
|
|
|
16
16
|
updatePayload: null,
|
|
17
17
|
row: {
|
|
18
18
|
workspace_id: 1,
|
|
19
|
-
name: "Workspace",
|
|
20
|
-
avatar_url: "",
|
|
21
19
|
light_primary_color: DEFAULT_WORKSPACE_THEME.light.color,
|
|
22
20
|
light_secondary_color: DEFAULT_WORKSPACE_THEME.light.secondaryColor,
|
|
23
21
|
light_surface_color: DEFAULT_WORKSPACE_THEME.light.surfaceColor,
|
|
@@ -41,8 +39,6 @@ function createKnexStub(rowOverrides = {}) {
|
|
|
41
39
|
state.insertedRow = { ...payload };
|
|
42
40
|
state.row = {
|
|
43
41
|
workspace_id: payload.workspace_id,
|
|
44
|
-
name: payload.name,
|
|
45
|
-
avatar_url: payload.avatar_url,
|
|
46
42
|
light_primary_color: payload.light_primary_color,
|
|
47
43
|
light_secondary_color: payload.light_secondary_color,
|
|
48
44
|
light_surface_color: payload.light_surface_color,
|
|
@@ -69,12 +65,6 @@ function createKnexStub(rowOverrides = {}) {
|
|
|
69
65
|
if (Object.hasOwn(payload, "invites_enabled")) {
|
|
70
66
|
state.row.invites_enabled = payload.invites_enabled;
|
|
71
67
|
}
|
|
72
|
-
if (Object.hasOwn(payload, "name")) {
|
|
73
|
-
state.row.name = payload.name;
|
|
74
|
-
}
|
|
75
|
-
if (Object.hasOwn(payload, "avatar_url")) {
|
|
76
|
-
state.row.avatar_url = payload.avatar_url;
|
|
77
|
-
}
|
|
78
68
|
if (Object.hasOwn(payload, "light_primary_color")) {
|
|
79
69
|
state.row.light_primary_color = payload.light_primary_color;
|
|
80
70
|
}
|
|
@@ -122,8 +112,6 @@ test("workspaceSettingsRepository.findByWorkspaceId maps the stored row", async
|
|
|
122
112
|
|
|
123
113
|
assert.deepEqual(record, {
|
|
124
114
|
workspaceId: 1,
|
|
125
|
-
name: "Workspace",
|
|
126
|
-
avatarUrl: "",
|
|
127
115
|
lightPrimaryColor: DEFAULT_WORKSPACE_THEME.light.color,
|
|
128
116
|
lightSecondaryColor: DEFAULT_WORKSPACE_THEME.light.secondaryColor,
|
|
129
117
|
lightSurfaceColor: DEFAULT_WORKSPACE_THEME.light.surfaceColor,
|
|
@@ -162,8 +150,6 @@ test("workspaceSettingsRepository.ensureForWorkspaceId inserts the injected defa
|
|
|
162
150
|
const record = await repository.ensureForWorkspaceId(5);
|
|
163
151
|
|
|
164
152
|
assert.equal(state.insertedRow.workspace_id, 5);
|
|
165
|
-
assert.equal(state.insertedRow.name, "Workspace");
|
|
166
|
-
assert.equal(state.insertedRow.avatar_url, "");
|
|
167
153
|
assert.equal(state.insertedRow.light_primary_color, DEFAULT_WORKSPACE_THEME.light.color);
|
|
168
154
|
assert.equal(state.insertedRow.light_secondary_color, DEFAULT_WORKSPACE_THEME.light.secondaryColor);
|
|
169
155
|
assert.equal(state.insertedRow.light_surface_color, DEFAULT_WORKSPACE_THEME.light.surfaceColor);
|
|
@@ -179,8 +165,6 @@ test("workspaceSettingsRepository.ensureForWorkspaceId inserts the injected defa
|
|
|
179
165
|
DEFAULT_WORKSPACE_THEME.dark.surfaceVariantColor
|
|
180
166
|
);
|
|
181
167
|
assert.equal(state.insertedRow.invites_enabled, false);
|
|
182
|
-
assert.equal(record.name, "Workspace");
|
|
183
|
-
assert.equal(record.avatarUrl, "");
|
|
184
168
|
assert.equal(record.lightPrimaryColor, DEFAULT_WORKSPACE_THEME.light.color);
|
|
185
169
|
assert.equal(record.lightSecondaryColor, DEFAULT_WORKSPACE_THEME.light.secondaryColor);
|
|
186
170
|
assert.equal(record.lightSurfaceColor, DEFAULT_WORKSPACE_THEME.light.surfaceColor);
|
|
@@ -192,23 +176,17 @@ test("workspaceSettingsRepository.ensureForWorkspaceId inserts the injected defa
|
|
|
192
176
|
assert.equal(record.invitesEnabled, false);
|
|
193
177
|
});
|
|
194
178
|
|
|
195
|
-
test("workspaceSettingsRepository.updateSettingsByWorkspaceId updates
|
|
179
|
+
test("workspaceSettingsRepository.updateSettingsByWorkspaceId updates workspace settings columns", async () => {
|
|
196
180
|
const { knexStub, state } = createKnexStub();
|
|
197
181
|
const repository = createRepository(knexStub, {
|
|
198
182
|
defaultInvitesEnabled: true
|
|
199
183
|
});
|
|
200
184
|
|
|
201
185
|
const updated = await repository.updateSettingsByWorkspaceId(1, {
|
|
202
|
-
name: "New name",
|
|
203
|
-
avatarUrl: "https://example.com/avatar.png",
|
|
204
186
|
lightPrimaryColor: "#123abc"
|
|
205
187
|
});
|
|
206
188
|
|
|
207
|
-
assert.equal(state.updatePayload.name, "New name");
|
|
208
|
-
assert.equal(state.updatePayload.avatar_url, "https://example.com/avatar.png");
|
|
209
189
|
assert.equal(state.updatePayload.light_primary_color, "#123ABC");
|
|
210
|
-
assert.equal(updated.name, "New name");
|
|
211
|
-
assert.equal(updated.avatarUrl, "https://example.com/avatar.png");
|
|
212
190
|
assert.equal(updated.lightPrimaryColor, "#123ABC");
|
|
213
191
|
});
|
|
214
192
|
|
|
@@ -46,8 +46,6 @@ function parseBody(operation, payload = {}) {
|
|
|
46
46
|
|
|
47
47
|
test("workspace settings patch body normalizes valid payload before validation", () => {
|
|
48
48
|
const parsed = parseBody(workspaceSettingsResource.operations.patch, {
|
|
49
|
-
name: " Team Mercury ",
|
|
50
|
-
avatarUrl: "https://example.com/avatar.png",
|
|
51
49
|
lightPrimaryColor: "#0f6b54",
|
|
52
50
|
lightSecondaryColor: "#0b4d3c",
|
|
53
51
|
lightSurfaceColor: "#eef5f3",
|
|
@@ -62,8 +60,6 @@ test("workspace settings patch body normalizes valid payload before validation",
|
|
|
62
60
|
assert.equal(parsed.ok, true);
|
|
63
61
|
assert.deepEqual(parsed.fieldErrors, {});
|
|
64
62
|
assert.deepEqual(parsed.value, {
|
|
65
|
-
name: "Team Mercury",
|
|
66
|
-
avatarUrl: "https://example.com/avatar.png",
|
|
67
63
|
lightPrimaryColor: "#0F6B54",
|
|
68
64
|
lightSecondaryColor: "#0B4D3C",
|
|
69
65
|
lightSurfaceColor: "#EEF5F3",
|
|
@@ -76,31 +72,18 @@ test("workspace settings patch body normalizes valid payload before validation",
|
|
|
76
72
|
});
|
|
77
73
|
});
|
|
78
74
|
|
|
79
|
-
test("workspace settings patch body
|
|
75
|
+
test("workspace settings patch body ignores unknown fields after normalization", () => {
|
|
80
76
|
const parsed = parseBody(workspaceSettingsResource.operations.patch, {
|
|
81
|
-
avatarUrl: "
|
|
77
|
+
avatarUrl: "https://example.com/avatar.png"
|
|
82
78
|
});
|
|
83
79
|
|
|
84
|
-
assert.equal(parsed.ok,
|
|
85
|
-
assert.
|
|
86
|
-
|
|
87
|
-
"Workspace avatar URL must be a valid absolute URL (http:// or https://)."
|
|
88
|
-
);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
test("workspace settings patch body keeps max-length name rule", () => {
|
|
92
|
-
const parsed = parseBody(workspaceSettingsResource.operations.patch, {
|
|
93
|
-
name: "x".repeat(161)
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
assert.equal(parsed.ok, false);
|
|
97
|
-
assert.equal(parsed.fieldErrors.name, "Workspace name must be at most 160 characters.");
|
|
80
|
+
assert.equal(parsed.ok, true);
|
|
81
|
+
assert.deepEqual(parsed.fieldErrors, {});
|
|
82
|
+
assert.deepEqual(parsed.value, {});
|
|
98
83
|
});
|
|
99
84
|
|
|
100
85
|
test("workspace settings create body requires full-write fields", () => {
|
|
101
|
-
const parsed = parseBody(workspaceSettingsResource.operations.create, {
|
|
102
|
-
name: "Mercury Workspace"
|
|
103
|
-
});
|
|
86
|
+
const parsed = parseBody(workspaceSettingsResource.operations.create, {});
|
|
104
87
|
|
|
105
88
|
assert.equal(parsed.ok, false);
|
|
106
89
|
assert.equal(parsed.fieldErrors.lightPrimaryColor, "Light primary color is required.");
|
|
@@ -125,8 +108,6 @@ test("workspace settings output normalizes raw service payloads", () => {
|
|
|
125
108
|
ownerUserId: "9"
|
|
126
109
|
},
|
|
127
110
|
settings: {
|
|
128
|
-
name: " Mercury Workspace ",
|
|
129
|
-
avatarUrl: " https://example.com/avatar.png ",
|
|
130
111
|
lightPrimaryColor: "#0f6b54",
|
|
131
112
|
invitesEnabled: false
|
|
132
113
|
},
|
|
@@ -140,8 +121,6 @@ test("workspace settings output normalizes raw service payloads", () => {
|
|
|
140
121
|
ownerUserId: 9
|
|
141
122
|
},
|
|
142
123
|
settings: {
|
|
143
|
-
name: "Mercury Workspace",
|
|
144
|
-
avatarUrl: "https://example.com/avatar.png",
|
|
145
124
|
lightPrimaryColor: "#0F6B54",
|
|
146
125
|
lightSecondaryColor: expectedTheme.light.secondaryColor,
|
|
147
126
|
lightSurfaceColor: expectedTheme.light.surfaceColor,
|
|
@@ -26,12 +26,9 @@ function createFixture({ workspaceInvitationsEnabled = true } = {}) {
|
|
|
26
26
|
slug: "tonymobily3",
|
|
27
27
|
name: "TonyMobily3",
|
|
28
28
|
ownerUserId: 9,
|
|
29
|
-
avatarUrl: "",
|
|
30
29
|
color: defaultTheme.light.color
|
|
31
30
|
},
|
|
32
31
|
settings: {
|
|
33
|
-
name: "TonyMobily3",
|
|
34
|
-
avatarUrl: "",
|
|
35
32
|
lightPrimaryColor: defaultTheme.light.color,
|
|
36
33
|
lightSecondaryColor: defaultTheme.light.secondaryColor,
|
|
37
34
|
lightSurfaceColor: defaultTheme.light.surfaceColor,
|
|
@@ -75,8 +72,6 @@ test("workspaceSettingsService.getWorkspaceSettings returns the stored invitesEn
|
|
|
75
72
|
);
|
|
76
73
|
|
|
77
74
|
assert.deepEqual(response.settings, {
|
|
78
|
-
name: "TonyMobily3",
|
|
79
|
-
avatarUrl: "",
|
|
80
75
|
lightPrimaryColor: "#0F6B54",
|
|
81
76
|
lightSecondaryColor: "#48A9A6",
|
|
82
77
|
lightSurfaceColor: "#FFFFFF",
|
|
@@ -97,19 +92,15 @@ test("workspaceSettingsService.updateWorkspaceSettings writes editable fields th
|
|
|
97
92
|
const response = await service.updateWorkspaceSettings(
|
|
98
93
|
state.workspace,
|
|
99
94
|
{
|
|
100
|
-
name: "New Name",
|
|
101
95
|
invitesEnabled: false
|
|
102
96
|
},
|
|
103
97
|
authorizedOptions(["workspace.settings.update"])
|
|
104
98
|
);
|
|
105
99
|
|
|
106
100
|
assert.deepEqual(state.settingsPatch, {
|
|
107
|
-
name: "New Name",
|
|
108
101
|
invitesEnabled: false
|
|
109
102
|
});
|
|
110
103
|
assert.deepEqual(response.settings, {
|
|
111
|
-
name: "New Name",
|
|
112
|
-
avatarUrl: "",
|
|
113
104
|
lightPrimaryColor: "#0F6B54",
|
|
114
105
|
lightSecondaryColor: "#48A9A6",
|
|
115
106
|
lightSurfaceColor: "#FFFFFF",
|
|
@@ -135,8 +126,6 @@ test("workspaceSettingsService disables invite settings in output when app polic
|
|
|
135
126
|
);
|
|
136
127
|
|
|
137
128
|
assert.deepEqual(response.settings, {
|
|
138
|
-
name: "TonyMobily3",
|
|
139
|
-
avatarUrl: "",
|
|
140
129
|
lightPrimaryColor: "#0F6B54",
|
|
141
130
|
lightSecondaryColor: "#48A9A6",
|
|
142
131
|
lightSurfaceColor: "#FFFFFF",
|