@jskit-ai/users-core 0.1.22 → 0.1.25
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 +15 -6
- package/package.json +6 -5
- package/src/server/common/formatters/workspaceFormatter.js +1 -7
- package/src/server/common/repositories/workspacesRepository.js +6 -18
- package/src/server/common/services/workspaceContextService.js +14 -6
- package/src/server/workspaceDirectory/bootWorkspaceDirectoryRoutes.js +64 -0
- package/src/server/workspaceDirectory/workspaceDirectoryActions.js +68 -0
- package/src/shared/resources/workspaceMembersResource.js +2 -4
- package/src/shared/resources/workspaceResource.js +35 -12
- package/templates/migrations/users_core_initial.cjs +1 -0
- package/templates/migrations/users_core_workspace_settings_single_name_source.cjs +23 -6
- package/templates/migrations/users_core_workspaces_drop_color.cjs +85 -0
- 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/workspaceMembersService.test.js +4 -8
- package/test/workspaceService.test.js +77 -15
- 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 +1 -7
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.25",
|
|
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,10 +257,19 @@ 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
|
},
|
|
264
|
+
{
|
|
265
|
+
op: "install-migration",
|
|
266
|
+
from: "templates/migrations/users_core_workspaces_drop_color.cjs",
|
|
267
|
+
toDir: "migrations",
|
|
268
|
+
extension: ".cjs",
|
|
269
|
+
reason: "Drop legacy workspaces.color now that workspace theme colors live in workspace_settings.",
|
|
270
|
+
category: "migration",
|
|
271
|
+
id: "users-core-workspaces-drop-color"
|
|
272
|
+
},
|
|
264
273
|
{
|
|
265
274
|
from: "templates/packages/main/src/shared/resources/workspaceSettingsFields.js",
|
|
266
275
|
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.25",
|
|
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"
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
coerceWorkspaceColor,
|
|
3
|
-
resolveWorkspaceThemePalettes
|
|
4
|
-
} from "../../../shared/settings.js";
|
|
1
|
+
import { resolveWorkspaceThemePalettes } from "../../../shared/settings.js";
|
|
5
2
|
import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/actions/textNormalization";
|
|
6
3
|
|
|
7
4
|
function mapWorkspaceSummary(workspace, membership) {
|
|
@@ -9,7 +6,6 @@ function mapWorkspaceSummary(workspace, membership) {
|
|
|
9
6
|
id: Number(workspace.id),
|
|
10
7
|
slug: normalizeText(workspace.slug),
|
|
11
8
|
name: normalizeText(workspace.name),
|
|
12
|
-
color: coerceWorkspaceColor(workspace.color),
|
|
13
9
|
avatarUrl: normalizeText(workspace.avatarUrl),
|
|
14
10
|
roleId: normalizeLowerText(membership?.roleId || "member") || "member",
|
|
15
11
|
isAccessible: normalizeLowerText(membership?.status || "active") === "active"
|
|
@@ -23,7 +19,6 @@ function mapWorkspaceSettingsPublic(workspaceSettings, { workspaceInvitationsEna
|
|
|
23
19
|
const themePalettes = resolveWorkspaceThemePalettes(source);
|
|
24
20
|
|
|
25
21
|
return {
|
|
26
|
-
name: normalizeText(source.name),
|
|
27
22
|
lightPrimaryColor: themePalettes.light.color,
|
|
28
23
|
lightSecondaryColor: themePalettes.light.secondaryColor,
|
|
29
24
|
lightSurfaceColor: themePalettes.light.surfaceColor,
|
|
@@ -32,7 +27,6 @@ function mapWorkspaceSettingsPublic(workspaceSettings, { workspaceInvitationsEna
|
|
|
32
27
|
darkSecondaryColor: themePalettes.dark.secondaryColor,
|
|
33
28
|
darkSurfaceColor: themePalettes.dark.surfaceColor,
|
|
34
29
|
darkSurfaceVariantColor: themePalettes.dark.surfaceVariantColor,
|
|
35
|
-
avatarUrl: normalizeText(source.avatarUrl),
|
|
36
30
|
invitesEnabled,
|
|
37
31
|
invitesAvailable,
|
|
38
32
|
invitesEffective: invitesAvailable && invitesEnabled
|
|
@@ -6,13 +6,11 @@ import {
|
|
|
6
6
|
nowDb,
|
|
7
7
|
isDuplicateEntryError
|
|
8
8
|
} from "./repositoryUtils.js";
|
|
9
|
-
import { coerceWorkspaceColor } from "../../../shared/settings.js";
|
|
10
9
|
|
|
11
10
|
function mapRow(row) {
|
|
12
11
|
if (!row) {
|
|
13
12
|
return null;
|
|
14
13
|
}
|
|
15
|
-
const color = coerceWorkspaceColor(row.color);
|
|
16
14
|
|
|
17
15
|
return {
|
|
18
16
|
id: Number(row.id),
|
|
@@ -21,7 +19,6 @@ function mapRow(row) {
|
|
|
21
19
|
ownerUserId: Number(row.owner_user_id),
|
|
22
20
|
isPersonal: Boolean(row.is_personal),
|
|
23
21
|
avatarUrl: row.avatar_url ? normalizeText(row.avatar_url) : "",
|
|
24
|
-
color,
|
|
25
22
|
createdAt: toIsoString(row.created_at),
|
|
26
23
|
updatedAt: toIsoString(row.updated_at),
|
|
27
24
|
deletedAt: toNullableIso(row.deleted_at)
|
|
@@ -45,15 +42,14 @@ function createRepository(knex) {
|
|
|
45
42
|
throw new TypeError("workspacesRepository requires knex.");
|
|
46
43
|
}
|
|
47
44
|
|
|
48
|
-
function workspaceSelectColumns(
|
|
45
|
+
function workspaceSelectColumns({ includeMembership = false } = {}) {
|
|
49
46
|
const columns = [
|
|
50
47
|
"w.id",
|
|
51
48
|
"w.slug",
|
|
52
49
|
"w.name",
|
|
53
50
|
"w.owner_user_id",
|
|
54
51
|
"w.is_personal",
|
|
55
|
-
|
|
56
|
-
"w.color",
|
|
52
|
+
"w.avatar_url",
|
|
57
53
|
"w.created_at",
|
|
58
54
|
"w.updated_at",
|
|
59
55
|
"w.deleted_at"
|
|
@@ -67,9 +63,8 @@ function createRepository(knex) {
|
|
|
67
63
|
async function findById(workspaceId, options = {}) {
|
|
68
64
|
const client = options?.trx || knex;
|
|
69
65
|
const row = await client("workspaces as w")
|
|
70
|
-
.leftJoin("workspace_settings as ws", "ws.workspace_id", "w.id")
|
|
71
66
|
.where({ "w.id": Number(workspaceId) })
|
|
72
|
-
.select(workspaceSelectColumns(
|
|
67
|
+
.select(workspaceSelectColumns())
|
|
73
68
|
.first();
|
|
74
69
|
return mapRow(row);
|
|
75
70
|
}
|
|
@@ -82,9 +77,8 @@ function createRepository(knex) {
|
|
|
82
77
|
}
|
|
83
78
|
|
|
84
79
|
const row = await client("workspaces as w")
|
|
85
|
-
.leftJoin("workspace_settings as ws", "ws.workspace_id", "w.id")
|
|
86
80
|
.where({ "w.slug": normalizedSlug })
|
|
87
|
-
.select(workspaceSelectColumns(
|
|
81
|
+
.select(workspaceSelectColumns())
|
|
88
82
|
.first();
|
|
89
83
|
return mapRow(row);
|
|
90
84
|
}
|
|
@@ -92,10 +86,9 @@ function createRepository(knex) {
|
|
|
92
86
|
async function findPersonalByOwnerUserId(userId, options = {}) {
|
|
93
87
|
const client = options?.trx || knex;
|
|
94
88
|
const row = await client("workspaces as w")
|
|
95
|
-
.leftJoin("workspace_settings as ws", "ws.workspace_id", "w.id")
|
|
96
89
|
.where({ "w.owner_user_id": Number(userId), "w.is_personal": 1 })
|
|
97
90
|
.orderBy("w.id", "asc")
|
|
98
|
-
.select(workspaceSelectColumns(
|
|
91
|
+
.select(workspaceSelectColumns())
|
|
99
92
|
.first();
|
|
100
93
|
return mapRow(row);
|
|
101
94
|
}
|
|
@@ -110,7 +103,6 @@ function createRepository(knex) {
|
|
|
110
103
|
owner_user_id: Number(source.ownerUserId),
|
|
111
104
|
is_personal: source.isPersonal ? 1 : 0,
|
|
112
105
|
avatar_url: normalizeText(source.avatarUrl),
|
|
113
|
-
color: coerceWorkspaceColor(source.color),
|
|
114
106
|
created_at: nowDb(),
|
|
115
107
|
updated_at: nowDb(),
|
|
116
108
|
deleted_at: null
|
|
@@ -149,9 +141,6 @@ function createRepository(knex) {
|
|
|
149
141
|
if (Object.hasOwn(source, "avatarUrl")) {
|
|
150
142
|
dbPatch.avatar_url = normalizeText(source.avatarUrl);
|
|
151
143
|
}
|
|
152
|
-
if (Object.hasOwn(source, "color")) {
|
|
153
|
-
dbPatch.color = coerceWorkspaceColor(source.color);
|
|
154
|
-
}
|
|
155
144
|
|
|
156
145
|
await client("workspaces").where({ id: Number(workspaceId) }).update(dbPatch);
|
|
157
146
|
return findById(workspaceId, { trx: client });
|
|
@@ -161,12 +150,11 @@ function createRepository(knex) {
|
|
|
161
150
|
const client = options?.trx || knex;
|
|
162
151
|
const rows = await client("workspace_memberships as wm")
|
|
163
152
|
.join("workspaces as w", "w.id", "wm.workspace_id")
|
|
164
|
-
.leftJoin("workspace_settings as ws", "ws.workspace_id", "w.id")
|
|
165
153
|
.where({ "wm.user_id": Number(userId) })
|
|
166
154
|
.whereNull("w.deleted_at")
|
|
167
155
|
.orderBy("w.is_personal", "desc")
|
|
168
156
|
.orderBy("w.id", "asc")
|
|
169
|
-
.select(workspaceSelectColumns(
|
|
157
|
+
.select(workspaceSelectColumns({ includeMembership: true }));
|
|
170
158
|
|
|
171
159
|
return rows.map(mapMembershipWorkspaceRow).filter(Boolean);
|
|
172
160
|
}
|
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
TENANCY_MODE_NONE,
|
|
5
5
|
resolveTenancyProfile
|
|
6
6
|
} from "../../../shared/tenancyProfile.js";
|
|
7
|
-
import { coerceWorkspaceColor } from "../../../shared/settings.js";
|
|
8
7
|
import {
|
|
9
8
|
resolveRolePermissions
|
|
10
9
|
} from "../../../shared/roles.js";
|
|
@@ -84,7 +83,6 @@ function createService({
|
|
|
84
83
|
const resolvedTenancyProfile = resolveTenancyProfile(appConfig);
|
|
85
84
|
const resolvedTenancyMode = resolvedTenancyProfile.mode;
|
|
86
85
|
const workspacePolicy = resolvedTenancyProfile.workspace;
|
|
87
|
-
const resolvedWorkspaceColor = coerceWorkspaceColor(appConfig.workspaceColor);
|
|
88
86
|
async function ensureUniqueWorkspaceSlug(baseSlug, options = {}) {
|
|
89
87
|
let suffix = 0;
|
|
90
88
|
while (suffix < 1000) {
|
|
@@ -125,8 +123,7 @@ function createService({
|
|
|
125
123
|
name: buildWorkspaceName(normalizedUser),
|
|
126
124
|
ownerUserId: normalizedUser.id,
|
|
127
125
|
isPersonal: true,
|
|
128
|
-
avatarUrl: ""
|
|
129
|
-
color: resolvedWorkspaceColor
|
|
126
|
+
avatarUrl: ""
|
|
130
127
|
},
|
|
131
128
|
options
|
|
132
129
|
);
|
|
@@ -194,8 +191,7 @@ function createService({
|
|
|
194
191
|
name: createInput.name,
|
|
195
192
|
ownerUserId: normalizedUser.id,
|
|
196
193
|
isPersonal: false,
|
|
197
|
-
avatarUrl: ""
|
|
198
|
-
color: resolvedWorkspaceColor
|
|
194
|
+
avatarUrl: ""
|
|
199
195
|
},
|
|
200
196
|
options
|
|
201
197
|
);
|
|
@@ -205,6 +201,16 @@ function createService({
|
|
|
205
201
|
return inserted;
|
|
206
202
|
}
|
|
207
203
|
|
|
204
|
+
async function getWorkspaceForAuthenticatedUser(user, workspaceSlug, options = {}) {
|
|
205
|
+
const workspaceContext = await resolveWorkspaceContextForUserBySlug(user, workspaceSlug, options);
|
|
206
|
+
return workspaceContext.workspace;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function updateWorkspaceForAuthenticatedUser(user, workspaceSlug, patch = {}, options = {}) {
|
|
210
|
+
const workspaceContext = await resolveWorkspaceContextForUserBySlug(user, workspaceSlug, options);
|
|
211
|
+
return workspacesRepository.updateById(workspaceContext.workspace.id, patch, options);
|
|
212
|
+
}
|
|
213
|
+
|
|
208
214
|
async function resolveWorkspaceContextForUserBySlug(user, workspaceSlug, options = {}) {
|
|
209
215
|
const normalizedUser = authenticatedUserValidator.normalize(user);
|
|
210
216
|
if (!normalizedUser) {
|
|
@@ -261,6 +267,8 @@ function createService({
|
|
|
261
267
|
ensurePersonalWorkspaceForUser,
|
|
262
268
|
provisionWorkspaceForNewUser,
|
|
263
269
|
createWorkspaceForAuthenticatedUser,
|
|
270
|
+
getWorkspaceForAuthenticatedUser,
|
|
271
|
+
updateWorkspaceForAuthenticatedUser,
|
|
264
272
|
listWorkspacesForUser,
|
|
265
273
|
listWorkspacesForAuthenticatedUser,
|
|
266
274
|
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
|
|
|
@@ -11,8 +11,7 @@ const workspaceSummaryOutputSchema = Type.Object(
|
|
|
11
11
|
slug: Type.String({ minLength: 1 }),
|
|
12
12
|
name: Type.String({ minLength: 1 }),
|
|
13
13
|
ownerUserId: Type.Integer({ minimum: 1 }),
|
|
14
|
-
avatarUrl: Type.String()
|
|
15
|
-
color: Type.String({ minLength: 1 })
|
|
14
|
+
avatarUrl: Type.String()
|
|
16
15
|
},
|
|
17
16
|
{ additionalProperties: false }
|
|
18
17
|
);
|
|
@@ -49,8 +48,7 @@ function normalizeWorkspaceAdminSummary(workspace) {
|
|
|
49
48
|
slug: normalizeText(source.slug),
|
|
50
49
|
name: normalizeText(source.name),
|
|
51
50
|
ownerUserId: Number(source.ownerUserId),
|
|
52
|
-
avatarUrl: normalizeText(source.avatarUrl)
|
|
53
|
-
color: normalizeText(source.color)
|
|
51
|
+
avatarUrl: normalizeText(source.avatarUrl)
|
|
54
52
|
};
|
|
55
53
|
}
|
|
56
54
|
|
|
@@ -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,11 +34,7 @@ function normalizeWorkspaceInput(payload = {}) {
|
|
|
19
34
|
normalized.ownerUserId = Number(source.ownerUserId);
|
|
20
35
|
}
|
|
21
36
|
if (Object.hasOwn(source, "avatarUrl")) {
|
|
22
|
-
normalized.avatarUrl =
|
|
23
|
-
}
|
|
24
|
-
if (Object.hasOwn(source, "color")) {
|
|
25
|
-
const color = normalizeText(source.color);
|
|
26
|
-
normalized.color = /^#[0-9A-Fa-f]{6}$/.test(color) ? color.toUpperCase() : null;
|
|
37
|
+
normalized.avatarUrl = normalizeWorkspaceAvatarUrl(source.avatarUrl);
|
|
27
38
|
}
|
|
28
39
|
if (Object.hasOwn(source, "isPersonal")) {
|
|
29
40
|
normalized.isPersonal = source.isPersonal === true;
|
|
@@ -40,8 +51,7 @@ function normalizeWorkspaceOutput(payload = {}) {
|
|
|
40
51
|
slug: normalizeLowerText(source.slug),
|
|
41
52
|
name: normalizeText(source.name),
|
|
42
53
|
ownerUserId: Number(source.ownerUserId),
|
|
43
|
-
avatarUrl: normalizeText(source.avatarUrl)
|
|
44
|
-
color: normalizeText(source.color).toUpperCase()
|
|
54
|
+
avatarUrl: normalizeText(source.avatarUrl)
|
|
45
55
|
};
|
|
46
56
|
}
|
|
47
57
|
|
|
@@ -52,7 +62,6 @@ function normalizeWorkspaceListItemOutput(payload = {}) {
|
|
|
52
62
|
id: Number(source.id),
|
|
53
63
|
slug: normalizeLowerText(source.slug),
|
|
54
64
|
name: normalizeText(source.name),
|
|
55
|
-
color: normalizeText(source.color).toUpperCase(),
|
|
56
65
|
avatarUrl: normalizeText(source.avatarUrl),
|
|
57
66
|
roleId: normalizeLowerText(source.roleId || "member") || "member",
|
|
58
67
|
isAccessible: source.isAccessible !== false
|
|
@@ -65,8 +74,7 @@ const responseRecordSchema = Type.Object(
|
|
|
65
74
|
slug: Type.String({ minLength: 1 }),
|
|
66
75
|
name: Type.String({ minLength: 1, maxLength: 160 }),
|
|
67
76
|
ownerUserId: Type.Integer({ minimum: 1 }),
|
|
68
|
-
avatarUrl: Type.String()
|
|
69
|
-
color: Type.String({ minLength: 7, maxLength: 7, pattern: "^#[0-9A-Fa-f]{6}$" })
|
|
77
|
+
avatarUrl: Type.String()
|
|
70
78
|
},
|
|
71
79
|
{ additionalProperties: false }
|
|
72
80
|
);
|
|
@@ -76,7 +84,6 @@ const listItemSchema = Type.Object(
|
|
|
76
84
|
id: Type.Integer({ minimum: 1 }),
|
|
77
85
|
slug: Type.String({ minLength: 1 }),
|
|
78
86
|
name: Type.String({ minLength: 1, maxLength: 160 }),
|
|
79
|
-
color: Type.String({ minLength: 7, maxLength: 7, pattern: "^#[0-9A-Fa-f]{6}$" }),
|
|
80
87
|
avatarUrl: Type.String(),
|
|
81
88
|
roleId: Type.String({ minLength: 1 }),
|
|
82
89
|
isAccessible: Type.Boolean()
|
|
@@ -92,6 +99,22 @@ const createRequestBodySchema = Type.Object(
|
|
|
92
99
|
{ additionalProperties: false }
|
|
93
100
|
);
|
|
94
101
|
|
|
102
|
+
const patchRequestBodySchema = Type.Object(
|
|
103
|
+
{
|
|
104
|
+
name: Type.Optional(Type.String({ minLength: 1, maxLength: 160 })),
|
|
105
|
+
avatarUrl: Type.Optional(
|
|
106
|
+
Type.String({
|
|
107
|
+
pattern: "^(https?://.+)?$",
|
|
108
|
+
messages: {
|
|
109
|
+
pattern: "Workspace avatar URL must be a valid absolute URL (http:// or https://).",
|
|
110
|
+
default: "Workspace avatar URL must be a valid absolute URL (http:// or https://)."
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
)
|
|
114
|
+
},
|
|
115
|
+
{ additionalProperties: false }
|
|
116
|
+
);
|
|
117
|
+
|
|
95
118
|
const responseRecordValidator = Object.freeze({
|
|
96
119
|
schema: responseRecordSchema,
|
|
97
120
|
normalize: normalizeWorkspaceOutput
|
|
@@ -138,7 +161,7 @@ const resource = {
|
|
|
138
161
|
patch: {
|
|
139
162
|
method: "PATCH",
|
|
140
163
|
bodyValidator: {
|
|
141
|
-
schema:
|
|
164
|
+
schema: patchRequestBodySchema,
|
|
142
165
|
normalize: normalizeWorkspaceInput
|
|
143
166
|
},
|
|
144
167
|
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
|
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const WORKSPACES_TABLE = "workspaces";
|
|
2
|
+
const WORKSPACE_SETTINGS_TABLE = "workspace_settings";
|
|
3
|
+
const LEGACY_COLOR_COLUMN = "color";
|
|
4
|
+
const SETTINGS_LIGHT_PRIMARY_COLOR_COLUMN = "light_primary_color";
|
|
5
|
+
const DEFAULT_WORKSPACE_COLOR = "#1867C0";
|
|
6
|
+
|
|
7
|
+
async function hasTable(knex, tableName) {
|
|
8
|
+
return knex.schema.hasTable(tableName);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function hasColumn(knex, tableName, columnName) {
|
|
12
|
+
return knex.schema.hasColumn(tableName, columnName);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeHexColor(value) {
|
|
16
|
+
const normalized = String(value || "").trim().toUpperCase();
|
|
17
|
+
return /^#[0-9A-F]{6}$/.test(normalized) ? normalized : "";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
exports.up = async function up(knex) {
|
|
21
|
+
const hasWorkspaces = await hasTable(knex, WORKSPACES_TABLE);
|
|
22
|
+
if (!hasWorkspaces) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const hasLegacyColor = await hasColumn(knex, WORKSPACES_TABLE, LEGACY_COLOR_COLUMN);
|
|
27
|
+
if (!hasLegacyColor) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
await knex.schema.alterTable(WORKSPACES_TABLE, (table) => {
|
|
32
|
+
table.dropColumn(LEGACY_COLOR_COLUMN);
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
exports.down = async function down(knex) {
|
|
37
|
+
const hasWorkspaces = await hasTable(knex, WORKSPACES_TABLE);
|
|
38
|
+
if (!hasWorkspaces) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const hasLegacyColor = await hasColumn(knex, WORKSPACES_TABLE, LEGACY_COLOR_COLUMN);
|
|
43
|
+
if (!hasLegacyColor) {
|
|
44
|
+
await knex.schema.alterTable(WORKSPACES_TABLE, (table) => {
|
|
45
|
+
table.string(LEGACY_COLOR_COLUMN, 7).notNullable().defaultTo(DEFAULT_WORKSPACE_COLOR);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const hasWorkspaceSettings = await hasTable(knex, WORKSPACE_SETTINGS_TABLE);
|
|
50
|
+
if (!hasWorkspaceSettings) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const hasLightPrimaryColor = await hasColumn(
|
|
55
|
+
knex,
|
|
56
|
+
WORKSPACE_SETTINGS_TABLE,
|
|
57
|
+
SETTINGS_LIGHT_PRIMARY_COLOR_COLUMN
|
|
58
|
+
);
|
|
59
|
+
if (!hasLightPrimaryColor) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const workspaceSettingsRows = await knex(WORKSPACE_SETTINGS_TABLE).select(
|
|
64
|
+
"workspace_id",
|
|
65
|
+
SETTINGS_LIGHT_PRIMARY_COLOR_COLUMN
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
for (const row of workspaceSettingsRows) {
|
|
69
|
+
const workspaceId = Number(row?.workspace_id || 0);
|
|
70
|
+
if (!Number.isInteger(workspaceId) || workspaceId < 1) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const restoredColor = normalizeHexColor(row?.[SETTINGS_LIGHT_PRIMARY_COLOR_COLUMN]);
|
|
75
|
+
if (!restoredColor) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
await knex(WORKSPACES_TABLE)
|
|
80
|
+
.where({ id: workspaceId })
|
|
81
|
+
.update({
|
|
82
|
+
color: restoredColor
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
};
|
|
@@ -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" }
|
|
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" }
|
|
486
|
+
}
|
|
417
487
|
});
|
|
418
488
|
});
|
|
419
489
|
|
|
@@ -42,8 +42,7 @@ function createFixture() {
|
|
|
42
42
|
slug: "tonymobily3",
|
|
43
43
|
name: "TonyMobily3",
|
|
44
44
|
ownerUserId: 9,
|
|
45
|
-
avatarUrl: ""
|
|
46
|
-
color: "#0F6B54"
|
|
45
|
+
avatarUrl: ""
|
|
47
46
|
};
|
|
48
47
|
|
|
49
48
|
const service = createService({
|
|
@@ -248,8 +247,7 @@ test("workspaceMembersService.listMembers uses the resolved workspace directly",
|
|
|
248
247
|
slug: "tonymobily3",
|
|
249
248
|
name: "TonyMobily3",
|
|
250
249
|
ownerUserId: 9,
|
|
251
|
-
avatarUrl: ""
|
|
252
|
-
color: "#0F6B54"
|
|
250
|
+
avatarUrl: ""
|
|
253
251
|
});
|
|
254
252
|
assert.equal(response.members.length, 1);
|
|
255
253
|
assert.equal(response.members[0].displayName, "Alice");
|
|
@@ -278,8 +276,7 @@ test("workspaceMembersService.removeMember marks membership revoked and returns
|
|
|
278
276
|
slug: "tonymobily3",
|
|
279
277
|
name: "TonyMobily3",
|
|
280
278
|
ownerUserId: 9,
|
|
281
|
-
avatarUrl: ""
|
|
282
|
-
color: "#0F6B54"
|
|
279
|
+
avatarUrl: ""
|
|
283
280
|
};
|
|
284
281
|
const service = createService({
|
|
285
282
|
workspaceMembershipsRepository: {
|
|
@@ -349,8 +346,7 @@ test("workspaceMembersService.removeMember rejects removing the owner", async ()
|
|
|
349
346
|
slug: "tonymobily3",
|
|
350
347
|
name: "TonyMobily3",
|
|
351
348
|
ownerUserId: 9,
|
|
352
|
-
avatarUrl: ""
|
|
353
|
-
color: "#0F6B54"
|
|
349
|
+
avatarUrl: ""
|
|
354
350
|
};
|
|
355
351
|
const service = createService({
|
|
356
352
|
workspaceMembershipsRepository: {
|
|
@@ -31,14 +31,14 @@ function createWorkspaceServiceFixture({
|
|
|
31
31
|
name: "TonyMobily3",
|
|
32
32
|
ownerUserId: 7,
|
|
33
33
|
isPersonal: true,
|
|
34
|
-
avatarUrl: ""
|
|
35
|
-
color: "#0F6B54"
|
|
34
|
+
avatarUrl: ""
|
|
36
35
|
}
|
|
37
36
|
} = {}) {
|
|
38
37
|
const calls = {
|
|
39
38
|
findPersonalByOwnerUserId: 0,
|
|
40
39
|
listForUserId: 0,
|
|
41
40
|
insert: 0,
|
|
41
|
+
updateById: 0,
|
|
42
42
|
ensureOwnerMembership: 0
|
|
43
43
|
};
|
|
44
44
|
let nextWorkspaceId = 10;
|
|
@@ -95,7 +95,6 @@ function createWorkspaceServiceFixture({
|
|
|
95
95
|
slug: "tonymobily3",
|
|
96
96
|
name: "TonyMobily3",
|
|
97
97
|
avatarUrl: "",
|
|
98
|
-
color: "#0F6B54",
|
|
99
98
|
roleId: "owner",
|
|
100
99
|
membershipStatus: "active"
|
|
101
100
|
},
|
|
@@ -104,7 +103,6 @@ function createWorkspaceServiceFixture({
|
|
|
104
103
|
slug: "pending-workspace",
|
|
105
104
|
name: "Pending Workspace",
|
|
106
105
|
avatarUrl: "",
|
|
107
|
-
color: "#0F6B54",
|
|
108
106
|
roleId: "member",
|
|
109
107
|
membershipStatus: "pending"
|
|
110
108
|
}
|
|
@@ -120,11 +118,33 @@ function createWorkspaceServiceFixture({
|
|
|
120
118
|
name: String(payload.name || ""),
|
|
121
119
|
ownerUserId: Number(payload.ownerUserId),
|
|
122
120
|
isPersonal: payload.isPersonal === true,
|
|
123
|
-
avatarUrl: String(payload.avatarUrl || "")
|
|
124
|
-
color: String(payload.color || "#0F6B54")
|
|
121
|
+
avatarUrl: String(payload.avatarUrl || "")
|
|
125
122
|
};
|
|
126
123
|
workspaceBySlug.set(String(inserted.slug).trim().toLowerCase(), inserted);
|
|
127
124
|
return inserted;
|
|
125
|
+
},
|
|
126
|
+
async updateById(workspaceId, patch) {
|
|
127
|
+
calls.updateById += 1;
|
|
128
|
+
const targetId = Number(workspaceId);
|
|
129
|
+
for (const [slug, workspace] of workspaceBySlug.entries()) {
|
|
130
|
+
if (Number(workspace.id) !== targetId) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const updated = {
|
|
134
|
+
...workspace
|
|
135
|
+
};
|
|
136
|
+
if (Object.hasOwn(patch, "name")) {
|
|
137
|
+
updated.name = String(patch.name || "");
|
|
138
|
+
}
|
|
139
|
+
if (Object.hasOwn(patch, "avatarUrl")) {
|
|
140
|
+
updated.avatarUrl = String(patch.avatarUrl || "");
|
|
141
|
+
}
|
|
142
|
+
workspaceBySlug.set(slug, updated);
|
|
143
|
+
return {
|
|
144
|
+
...updated
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
128
148
|
}
|
|
129
149
|
},
|
|
130
150
|
workspaceMembershipsRepository: {
|
|
@@ -200,7 +220,6 @@ test("workspaceService.listWorkspacesForUser returns all active memberships in p
|
|
|
200
220
|
slug: "chiaramobily",
|
|
201
221
|
name: "Chiara Personal",
|
|
202
222
|
avatarUrl: "",
|
|
203
|
-
color: "#0F6B54",
|
|
204
223
|
roleId: "owner",
|
|
205
224
|
membershipStatus: "active"
|
|
206
225
|
},
|
|
@@ -209,7 +228,6 @@ test("workspaceService.listWorkspacesForUser returns all active memberships in p
|
|
|
209
228
|
slug: "tonymobily",
|
|
210
229
|
name: "Tony Workspace",
|
|
211
230
|
avatarUrl: "",
|
|
212
|
-
color: "#0F6B54",
|
|
213
231
|
roleId: "member",
|
|
214
232
|
membershipStatus: "active"
|
|
215
233
|
},
|
|
@@ -218,7 +236,6 @@ test("workspaceService.listWorkspacesForUser returns all active memberships in p
|
|
|
218
236
|
slug: "pending-workspace",
|
|
219
237
|
name: "Pending Workspace",
|
|
220
238
|
avatarUrl: "",
|
|
221
|
-
color: "#0F6B54",
|
|
222
239
|
roleId: "member",
|
|
223
240
|
membershipStatus: "pending"
|
|
224
241
|
}
|
|
@@ -352,8 +369,7 @@ test("workspaceService.resolveWorkspaceContextForUserBySlug allows personal tena
|
|
|
352
369
|
name: "My Personal",
|
|
353
370
|
ownerUserId: 7,
|
|
354
371
|
isPersonal: true,
|
|
355
|
-
avatarUrl: ""
|
|
356
|
-
color: "#0F6B54"
|
|
372
|
+
avatarUrl: ""
|
|
357
373
|
},
|
|
358
374
|
additionalWorkspaces: [
|
|
359
375
|
{
|
|
@@ -362,8 +378,7 @@ test("workspaceService.resolveWorkspaceContextForUserBySlug allows personal tena
|
|
|
362
378
|
name: "Team Alpha",
|
|
363
379
|
ownerUserId: 99,
|
|
364
380
|
isPersonal: false,
|
|
365
|
-
avatarUrl: ""
|
|
366
|
-
color: "#0F6B54"
|
|
381
|
+
avatarUrl: ""
|
|
367
382
|
}
|
|
368
383
|
]
|
|
369
384
|
});
|
|
@@ -402,8 +417,7 @@ test("workspaceService.resolveWorkspaceContextForUserBySlug grants owner access
|
|
|
402
417
|
name: "TonyMobily",
|
|
403
418
|
ownerUserId: 7,
|
|
404
419
|
isPersonal: true,
|
|
405
|
-
avatarUrl: ""
|
|
406
|
-
color: "#0F6B54"
|
|
420
|
+
avatarUrl: ""
|
|
407
421
|
};
|
|
408
422
|
},
|
|
409
423
|
async findPersonalByOwnerUserId() {
|
|
@@ -478,3 +492,51 @@ test("workspaceService.resolveWorkspaceContextForUserBySlug resolves permissions
|
|
|
478
492
|
|
|
479
493
|
assert.deepEqual(context.permissions, ["workspace.settings.update"]);
|
|
480
494
|
});
|
|
495
|
+
|
|
496
|
+
test("workspaceService.getWorkspaceForAuthenticatedUser resolves workspace from slug context", async () => {
|
|
497
|
+
const { service } = createWorkspaceServiceFixture({
|
|
498
|
+
additionalWorkspaces: [
|
|
499
|
+
{
|
|
500
|
+
id: 42,
|
|
501
|
+
slug: "team-alpha",
|
|
502
|
+
name: "Team Alpha",
|
|
503
|
+
ownerUserId: 99,
|
|
504
|
+
isPersonal: false,
|
|
505
|
+
avatarUrl: ""
|
|
506
|
+
}
|
|
507
|
+
]
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
const workspace = await service.getWorkspaceForAuthenticatedUser(
|
|
511
|
+
{
|
|
512
|
+
id: 7,
|
|
513
|
+
email: "chiaramobily@gmail.com",
|
|
514
|
+
displayName: "Chiara"
|
|
515
|
+
},
|
|
516
|
+
"team-alpha"
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
assert.equal(workspace.slug, "team-alpha");
|
|
520
|
+
assert.equal(workspace.name, "Team Alpha");
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
test("workspaceService.updateWorkspaceForAuthenticatedUser updates workspace profile fields", async () => {
|
|
524
|
+
const { service, calls } = createWorkspaceServiceFixture();
|
|
525
|
+
|
|
526
|
+
const workspace = await service.updateWorkspaceForAuthenticatedUser(
|
|
527
|
+
{
|
|
528
|
+
id: 7,
|
|
529
|
+
email: "chiaramobily@gmail.com",
|
|
530
|
+
displayName: "Chiara"
|
|
531
|
+
},
|
|
532
|
+
"tonymobily3",
|
|
533
|
+
{
|
|
534
|
+
name: "Updated Workspace",
|
|
535
|
+
avatarUrl: "https://example.com/acme.png"
|
|
536
|
+
}
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
assert.equal(calls.updateById, 1);
|
|
540
|
+
assert.equal(workspace.name, "Updated Workspace");
|
|
541
|
+
assert.equal(workspace.avatarUrl, "https://example.com/acme.png");
|
|
542
|
+
});
|
|
@@ -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,
|
|
@@ -25,12 +25,9 @@ function createFixture({ workspaceInvitationsEnabled = true } = {}) {
|
|
|
25
25
|
id: 7,
|
|
26
26
|
slug: "tonymobily3",
|
|
27
27
|
name: "TonyMobily3",
|
|
28
|
-
ownerUserId: 9
|
|
29
|
-
avatarUrl: "",
|
|
30
|
-
color: defaultTheme.light.color
|
|
28
|
+
ownerUserId: 9
|
|
31
29
|
},
|
|
32
30
|
settings: {
|
|
33
|
-
avatarUrl: "",
|
|
34
31
|
lightPrimaryColor: defaultTheme.light.color,
|
|
35
32
|
lightSecondaryColor: defaultTheme.light.secondaryColor,
|
|
36
33
|
lightSurfaceColor: defaultTheme.light.surfaceColor,
|
|
@@ -74,7 +71,6 @@ test("workspaceSettingsService.getWorkspaceSettings returns the stored invitesEn
|
|
|
74
71
|
);
|
|
75
72
|
|
|
76
73
|
assert.deepEqual(response.settings, {
|
|
77
|
-
avatarUrl: "",
|
|
78
74
|
lightPrimaryColor: "#0F6B54",
|
|
79
75
|
lightSecondaryColor: "#48A9A6",
|
|
80
76
|
lightSurfaceColor: "#FFFFFF",
|
|
@@ -104,7 +100,6 @@ test("workspaceSettingsService.updateWorkspaceSettings writes editable fields th
|
|
|
104
100
|
invitesEnabled: false
|
|
105
101
|
});
|
|
106
102
|
assert.deepEqual(response.settings, {
|
|
107
|
-
avatarUrl: "",
|
|
108
103
|
lightPrimaryColor: "#0F6B54",
|
|
109
104
|
lightSecondaryColor: "#48A9A6",
|
|
110
105
|
lightSurfaceColor: "#FFFFFF",
|
|
@@ -130,7 +125,6 @@ test("workspaceSettingsService disables invite settings in output when app polic
|
|
|
130
125
|
);
|
|
131
126
|
|
|
132
127
|
assert.deepEqual(response.settings, {
|
|
133
|
-
avatarUrl: "",
|
|
134
128
|
lightPrimaryColor: "#0F6B54",
|
|
135
129
|
lightSecondaryColor: "#48A9A6",
|
|
136
130
|
lightSurfaceColor: "#FFFFFF",
|