@jskit-ai/users-core 0.1.22 → 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 +6 -6
- package/package.json +6 -5
- package/src/server/common/formatters/workspaceFormatter.js +0 -2
- package/src/server/common/repositories/workspacesRepository.js +6 -10
- 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_initial.cjs +1 -0
- package/templates/migrations/users_core_workspace_settings_single_name_source.cjs +23 -6
- package/templates/packages/main/src/shared/resources/workspaceSettingsFields.js +0 -32
- 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 -12
- package/test/workspaceSettingsResource.test.js +5 -11
- package/test/workspaceSettingsService.test.js +0 -5
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"
|
|
@@ -257,7 +257,7 @@ export default Object.freeze({
|
|
|
257
257
|
from: "templates/migrations/users_core_workspace_settings_single_name_source.cjs",
|
|
258
258
|
toDir: "migrations",
|
|
259
259
|
extension: ".cjs",
|
|
260
|
-
reason: "Remove workspace_settings
|
|
260
|
+
reason: "Remove workspace_settings name/avatar fields so workspace identity data comes from workspaces only.",
|
|
261
261
|
category: "migration",
|
|
262
262
|
id: "users-core-workspace-settings-single-name-source"
|
|
263
263
|
},
|
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
|
|
@@ -44,6 +44,7 @@ exports.up = async function up(knex) {
|
|
|
44
44
|
|
|
45
45
|
await knex.schema.createTable("workspace_settings", (table) => {
|
|
46
46
|
table.integer("workspace_id").unsigned().primary().references("id").inTable("workspaces").onDelete("CASCADE");
|
|
47
|
+
table.string("name", 160).notNullable().defaultTo("Workspace");
|
|
47
48
|
table.string("avatar_url", 512).notNullable().defaultTo("");
|
|
48
49
|
table.string("light_primary_color", 7).notNullable().defaultTo("#1867C0");
|
|
49
50
|
table.string("light_secondary_color", 7).notNullable().defaultTo("#48A9A6");
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const WORKSPACE_SETTINGS_TABLE = "workspace_settings";
|
|
2
2
|
const WORKSPACES_TABLE = "workspaces";
|
|
3
3
|
const LEGACY_NAME_COLUMN = "name";
|
|
4
|
+
const LEGACY_AVATAR_COLUMN = "avatar_url";
|
|
4
5
|
|
|
5
6
|
async function hasTable(knex, tableName) {
|
|
6
7
|
return knex.schema.hasTable(tableName);
|
|
@@ -17,12 +18,18 @@ exports.up = async function up(knex) {
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
const hasLegacyName = await hasColumn(knex, WORKSPACE_SETTINGS_TABLE, LEGACY_NAME_COLUMN);
|
|
20
|
-
|
|
21
|
+
const hasLegacyAvatarUrl = await hasColumn(knex, WORKSPACE_SETTINGS_TABLE, LEGACY_AVATAR_COLUMN);
|
|
22
|
+
if (!hasLegacyName && !hasLegacyAvatarUrl) {
|
|
21
23
|
return;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
await knex.schema.alterTable(WORKSPACE_SETTINGS_TABLE, (table) => {
|
|
25
|
-
|
|
27
|
+
if (hasLegacyName) {
|
|
28
|
+
table.dropColumn(LEGACY_NAME_COLUMN);
|
|
29
|
+
}
|
|
30
|
+
if (hasLegacyAvatarUrl) {
|
|
31
|
+
table.dropColumn(LEGACY_AVATAR_COLUMN);
|
|
32
|
+
}
|
|
26
33
|
});
|
|
27
34
|
};
|
|
28
35
|
|
|
@@ -33,9 +40,15 @@ exports.down = async function down(knex) {
|
|
|
33
40
|
}
|
|
34
41
|
|
|
35
42
|
const hasLegacyName = await hasColumn(knex, WORKSPACE_SETTINGS_TABLE, LEGACY_NAME_COLUMN);
|
|
36
|
-
|
|
43
|
+
const hasLegacyAvatarUrl = await hasColumn(knex, WORKSPACE_SETTINGS_TABLE, LEGACY_AVATAR_COLUMN);
|
|
44
|
+
if (!hasLegacyName || !hasLegacyAvatarUrl) {
|
|
37
45
|
await knex.schema.alterTable(WORKSPACE_SETTINGS_TABLE, (table) => {
|
|
38
|
-
|
|
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
|
+
}
|
|
39
52
|
});
|
|
40
53
|
}
|
|
41
54
|
|
|
@@ -44,11 +57,15 @@ exports.down = async function down(knex) {
|
|
|
44
57
|
return;
|
|
45
58
|
}
|
|
46
59
|
|
|
47
|
-
const workspaceRows = await knex(WORKSPACES_TABLE).select("id", "name");
|
|
60
|
+
const workspaceRows = await knex(WORKSPACES_TABLE).select("id", "name", "avatar_url");
|
|
48
61
|
for (const workspaceRow of workspaceRows) {
|
|
49
62
|
const normalizedName = String(workspaceRow?.name || "").trim() || "Workspace";
|
|
63
|
+
const normalizedAvatarUrl = String(workspaceRow?.avatar_url || "").trim();
|
|
50
64
|
await knex(WORKSPACE_SETTINGS_TABLE)
|
|
51
65
|
.where({ workspace_id: Number(workspaceRow.id) })
|
|
52
|
-
.update({
|
|
66
|
+
.update({
|
|
67
|
+
name: normalizedName,
|
|
68
|
+
avatar_url: normalizedAvatarUrl
|
|
69
|
+
});
|
|
53
70
|
}
|
|
54
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,23 +20,6 @@ function normalizeHexColor(value) {
|
|
|
35
20
|
|
|
36
21
|
resetWorkspaceSettingsFields();
|
|
37
22
|
|
|
38
|
-
defineField({
|
|
39
|
-
key: "avatarUrl",
|
|
40
|
-
dbColumn: "avatar_url",
|
|
41
|
-
required: false,
|
|
42
|
-
inputSchema: Type.String({
|
|
43
|
-
pattern: "^(https?://.+)?$",
|
|
44
|
-
messages: {
|
|
45
|
-
pattern: "Workspace avatar URL must be a valid absolute URL (http:// or https://).",
|
|
46
|
-
default: "Workspace avatar URL must be a valid absolute URL (http:// or https://)."
|
|
47
|
-
}
|
|
48
|
-
}),
|
|
49
|
-
outputSchema: Type.String(),
|
|
50
|
-
normalizeInput: normalizeAvatarUrl,
|
|
51
|
-
normalizeOutput: (value) => normalizeText(value),
|
|
52
|
-
resolveDefault: ({ workspace = {} } = {}) => normalizeText(workspace.avatarUrl)
|
|
53
|
-
});
|
|
54
|
-
|
|
55
23
|
defineField({
|
|
56
24
|
key: "lightPrimaryColor",
|
|
57
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,7 +16,6 @@ function createKnexStub(rowOverrides = {}) {
|
|
|
16
16
|
updatePayload: null,
|
|
17
17
|
row: {
|
|
18
18
|
workspace_id: 1,
|
|
19
|
-
avatar_url: "",
|
|
20
19
|
light_primary_color: DEFAULT_WORKSPACE_THEME.light.color,
|
|
21
20
|
light_secondary_color: DEFAULT_WORKSPACE_THEME.light.secondaryColor,
|
|
22
21
|
light_surface_color: DEFAULT_WORKSPACE_THEME.light.surfaceColor,
|
|
@@ -40,7 +39,6 @@ function createKnexStub(rowOverrides = {}) {
|
|
|
40
39
|
state.insertedRow = { ...payload };
|
|
41
40
|
state.row = {
|
|
42
41
|
workspace_id: payload.workspace_id,
|
|
43
|
-
avatar_url: payload.avatar_url,
|
|
44
42
|
light_primary_color: payload.light_primary_color,
|
|
45
43
|
light_secondary_color: payload.light_secondary_color,
|
|
46
44
|
light_surface_color: payload.light_surface_color,
|
|
@@ -67,9 +65,6 @@ function createKnexStub(rowOverrides = {}) {
|
|
|
67
65
|
if (Object.hasOwn(payload, "invites_enabled")) {
|
|
68
66
|
state.row.invites_enabled = payload.invites_enabled;
|
|
69
67
|
}
|
|
70
|
-
if (Object.hasOwn(payload, "avatar_url")) {
|
|
71
|
-
state.row.avatar_url = payload.avatar_url;
|
|
72
|
-
}
|
|
73
68
|
if (Object.hasOwn(payload, "light_primary_color")) {
|
|
74
69
|
state.row.light_primary_color = payload.light_primary_color;
|
|
75
70
|
}
|
|
@@ -117,7 +112,6 @@ test("workspaceSettingsRepository.findByWorkspaceId maps the stored row", async
|
|
|
117
112
|
|
|
118
113
|
assert.deepEqual(record, {
|
|
119
114
|
workspaceId: 1,
|
|
120
|
-
avatarUrl: "",
|
|
121
115
|
lightPrimaryColor: DEFAULT_WORKSPACE_THEME.light.color,
|
|
122
116
|
lightSecondaryColor: DEFAULT_WORKSPACE_THEME.light.secondaryColor,
|
|
123
117
|
lightSurfaceColor: DEFAULT_WORKSPACE_THEME.light.surfaceColor,
|
|
@@ -156,7 +150,6 @@ test("workspaceSettingsRepository.ensureForWorkspaceId inserts the injected defa
|
|
|
156
150
|
const record = await repository.ensureForWorkspaceId(5);
|
|
157
151
|
|
|
158
152
|
assert.equal(state.insertedRow.workspace_id, 5);
|
|
159
|
-
assert.equal(state.insertedRow.avatar_url, "");
|
|
160
153
|
assert.equal(state.insertedRow.light_primary_color, DEFAULT_WORKSPACE_THEME.light.color);
|
|
161
154
|
assert.equal(state.insertedRow.light_secondary_color, DEFAULT_WORKSPACE_THEME.light.secondaryColor);
|
|
162
155
|
assert.equal(state.insertedRow.light_surface_color, DEFAULT_WORKSPACE_THEME.light.surfaceColor);
|
|
@@ -172,7 +165,6 @@ test("workspaceSettingsRepository.ensureForWorkspaceId inserts the injected defa
|
|
|
172
165
|
DEFAULT_WORKSPACE_THEME.dark.surfaceVariantColor
|
|
173
166
|
);
|
|
174
167
|
assert.equal(state.insertedRow.invites_enabled, false);
|
|
175
|
-
assert.equal(record.avatarUrl, "");
|
|
176
168
|
assert.equal(record.lightPrimaryColor, DEFAULT_WORKSPACE_THEME.light.color);
|
|
177
169
|
assert.equal(record.lightSecondaryColor, DEFAULT_WORKSPACE_THEME.light.secondaryColor);
|
|
178
170
|
assert.equal(record.lightSurfaceColor, DEFAULT_WORKSPACE_THEME.light.surfaceColor);
|
|
@@ -184,20 +176,17 @@ test("workspaceSettingsRepository.ensureForWorkspaceId inserts the injected defa
|
|
|
184
176
|
assert.equal(record.invitesEnabled, false);
|
|
185
177
|
});
|
|
186
178
|
|
|
187
|
-
test("workspaceSettingsRepository.updateSettingsByWorkspaceId updates
|
|
179
|
+
test("workspaceSettingsRepository.updateSettingsByWorkspaceId updates workspace settings columns", async () => {
|
|
188
180
|
const { knexStub, state } = createKnexStub();
|
|
189
181
|
const repository = createRepository(knexStub, {
|
|
190
182
|
defaultInvitesEnabled: true
|
|
191
183
|
});
|
|
192
184
|
|
|
193
185
|
const updated = await repository.updateSettingsByWorkspaceId(1, {
|
|
194
|
-
avatarUrl: "https://example.com/avatar.png",
|
|
195
186
|
lightPrimaryColor: "#123abc"
|
|
196
187
|
});
|
|
197
188
|
|
|
198
|
-
assert.equal(state.updatePayload.avatar_url, "https://example.com/avatar.png");
|
|
199
189
|
assert.equal(state.updatePayload.light_primary_color, "#123ABC");
|
|
200
|
-
assert.equal(updated.avatarUrl, "https://example.com/avatar.png");
|
|
201
190
|
assert.equal(updated.lightPrimaryColor, "#123ABC");
|
|
202
191
|
});
|
|
203
192
|
|
|
@@ -46,7 +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
|
-
avatarUrl: "https://example.com/avatar.png",
|
|
50
49
|
lightPrimaryColor: "#0f6b54",
|
|
51
50
|
lightSecondaryColor: "#0b4d3c",
|
|
52
51
|
lightSurfaceColor: "#eef5f3",
|
|
@@ -61,7 +60,6 @@ test("workspace settings patch body normalizes valid payload before validation",
|
|
|
61
60
|
assert.equal(parsed.ok, true);
|
|
62
61
|
assert.deepEqual(parsed.fieldErrors, {});
|
|
63
62
|
assert.deepEqual(parsed.value, {
|
|
64
|
-
avatarUrl: "https://example.com/avatar.png",
|
|
65
63
|
lightPrimaryColor: "#0F6B54",
|
|
66
64
|
lightSecondaryColor: "#0B4D3C",
|
|
67
65
|
lightSurfaceColor: "#EEF5F3",
|
|
@@ -74,16 +72,14 @@ test("workspace settings patch body normalizes valid payload before validation",
|
|
|
74
72
|
});
|
|
75
73
|
});
|
|
76
74
|
|
|
77
|
-
test("workspace settings patch body
|
|
75
|
+
test("workspace settings patch body ignores unknown fields after normalization", () => {
|
|
78
76
|
const parsed = parseBody(workspaceSettingsResource.operations.patch, {
|
|
79
|
-
avatarUrl: "
|
|
77
|
+
avatarUrl: "https://example.com/avatar.png"
|
|
80
78
|
});
|
|
81
79
|
|
|
82
|
-
assert.equal(parsed.ok,
|
|
83
|
-
assert.
|
|
84
|
-
|
|
85
|
-
"Workspace avatar URL must be a valid absolute URL (http:// or https://)."
|
|
86
|
-
);
|
|
80
|
+
assert.equal(parsed.ok, true);
|
|
81
|
+
assert.deepEqual(parsed.fieldErrors, {});
|
|
82
|
+
assert.deepEqual(parsed.value, {});
|
|
87
83
|
});
|
|
88
84
|
|
|
89
85
|
test("workspace settings create body requires full-write fields", () => {
|
|
@@ -112,7 +108,6 @@ test("workspace settings output normalizes raw service payloads", () => {
|
|
|
112
108
|
ownerUserId: "9"
|
|
113
109
|
},
|
|
114
110
|
settings: {
|
|
115
|
-
avatarUrl: " https://example.com/avatar.png ",
|
|
116
111
|
lightPrimaryColor: "#0f6b54",
|
|
117
112
|
invitesEnabled: false
|
|
118
113
|
},
|
|
@@ -126,7 +121,6 @@ test("workspace settings output normalizes raw service payloads", () => {
|
|
|
126
121
|
ownerUserId: 9
|
|
127
122
|
},
|
|
128
123
|
settings: {
|
|
129
|
-
avatarUrl: "https://example.com/avatar.png",
|
|
130
124
|
lightPrimaryColor: "#0F6B54",
|
|
131
125
|
lightSecondaryColor: expectedTheme.light.secondaryColor,
|
|
132
126
|
lightSurfaceColor: expectedTheme.light.surfaceColor,
|
|
@@ -26,11 +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
|
-
avatarUrl: "",
|
|
34
32
|
lightPrimaryColor: defaultTheme.light.color,
|
|
35
33
|
lightSecondaryColor: defaultTheme.light.secondaryColor,
|
|
36
34
|
lightSurfaceColor: defaultTheme.light.surfaceColor,
|
|
@@ -74,7 +72,6 @@ test("workspaceSettingsService.getWorkspaceSettings returns the stored invitesEn
|
|
|
74
72
|
);
|
|
75
73
|
|
|
76
74
|
assert.deepEqual(response.settings, {
|
|
77
|
-
avatarUrl: "",
|
|
78
75
|
lightPrimaryColor: "#0F6B54",
|
|
79
76
|
lightSecondaryColor: "#48A9A6",
|
|
80
77
|
lightSurfaceColor: "#FFFFFF",
|
|
@@ -104,7 +101,6 @@ test("workspaceSettingsService.updateWorkspaceSettings writes editable fields th
|
|
|
104
101
|
invitesEnabled: false
|
|
105
102
|
});
|
|
106
103
|
assert.deepEqual(response.settings, {
|
|
107
|
-
avatarUrl: "",
|
|
108
104
|
lightPrimaryColor: "#0F6B54",
|
|
109
105
|
lightSecondaryColor: "#48A9A6",
|
|
110
106
|
lightSurfaceColor: "#FFFFFF",
|
|
@@ -130,7 +126,6 @@ test("workspaceSettingsService disables invite settings in output when app polic
|
|
|
130
126
|
);
|
|
131
127
|
|
|
132
128
|
assert.deepEqual(response.settings, {
|
|
133
|
-
avatarUrl: "",
|
|
134
129
|
lightPrimaryColor: "#0F6B54",
|
|
135
130
|
lightSecondaryColor: "#48A9A6",
|
|
136
131
|
lightSurfaceColor: "#FFFFFF",
|