@jskit-ai/workspaces-core 0.2.3 → 0.2.5
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/locales/en.json +2 -2
- package/package.json +38 -11
- package/patterns/workspace-server/PATTERN.md +42 -17
- package/patterns/workspace-server/example/packages/main/src/server/WorkspaceInvitationsFeature.js +27 -0
- package/patterns/workspace-server/example/packages/main/src/server/email/workspaceInviteEmail.js +11 -10
- package/patterns/workspace-server/example/packages/main/src/server/email/workspaceInviteMailer.js +27 -0
- package/src/server/WorkspacesFeature.js +74 -183
- package/src/server/WorkspacesIntegrationsProvider.js +26 -10
- package/src/server/common/contributors/workspaceActionContextContributor.js +60 -70
- package/src/server/common/contributors/workspaceAuthPolicyContextResolver.js +12 -27
- package/src/server/common/contributors/workspaceRouteVisibilityResolver.js +48 -63
- package/src/server/common/formatters/workspaceFormatter.js +12 -10
- package/src/server/common/repositories/workspaceInvitesRepository.js +74 -75
- package/src/server/common/repositories/workspaceMembershipsRepository.js +23 -45
- package/src/server/common/repositories/workspacesRepository.js +17 -25
- package/src/server/common/services/{workspaceContextService.js → workspaceDirectoryService.js} +52 -72
- package/src/server/common/support/realtimeServiceEvents.js +20 -12
- package/src/server/common/support/workspaceRecordId.js +21 -0
- package/src/server/common/validators/routeParamsValidator.js +3 -2
- package/src/server/i18n.js +5 -2
- package/src/server/support/resolveWorkspace.js +10 -7
- package/src/server/support/workspaceActionSurfaces.js +5 -41
- package/src/server/support/workspaceCrud.js +25 -0
- package/src/server/support/workspaceInvitationsPolicy.js +16 -27
- package/src/server/support/workspaceRouteInput.js +19 -2
- package/src/server/{common/support → support}/workspaceRoutePaths.js +8 -2
- package/src/server/types.js +47 -0
- package/src/server/workspaceBootstrapContributor.js +105 -142
- package/src/server/workspaceDirectory/workspaceDirectoryActions.js +105 -137
- package/src/server/workspaceDirectory/{bootWorkspaceDirectoryRoutes.js → workspaceDirectoryRoutes.js} +7 -19
- package/src/server/workspaceMembers/defaultWorkspaceInviteEmail.js +15 -29
- package/src/server/workspaceMembers/unconfiguredWorkspaceInviteMailer.js +25 -0
- package/src/server/workspaceMembers/workspaceInviteUrls.js +24 -23
- package/src/server/workspaceMembers/workspaceMembersActions.js +189 -219
- package/src/server/workspaceMembers/{bootWorkspaceMembers.js → workspaceMembersRoutes.js} +14 -26
- package/src/server/workspaceMembers/workspaceMembersService.js +110 -97
- package/src/server/workspacePendingInvitations/noInviteAcceptanceParticipant.js +8 -0
- package/src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js +79 -112
- package/src/server/workspacePendingInvitations/{bootWorkspacePendingInvitations.js → workspacePendingInvitationsRoutes.js} +11 -8
- package/src/server/workspacePendingInvitations/workspacePendingInvitationsService.js +54 -57
- package/src/server/workspaceSettings/workspaceSettingsActions.js +53 -73
- package/src/server/workspaceSettings/workspaceSettingsRepository.js +20 -20
- package/src/server/workspaceSettings/{bootWorkspaceSettings.js → workspaceSettingsRoutes.js} +9 -21
- package/src/server/workspaceSettings/workspaceSettingsService.js +24 -26
- package/src/server/workspacesRuntime.js +185 -0
- package/src/shared/operationMessages.js +6 -15
- package/src/shared/resources/workspaceMembersResource.js +2 -2
- package/src/shared/resources/workspacePendingInvitationsResource.js +2 -2
- package/src/shared/resources/workspaceResource.js +4 -1
- package/src/shared/roles.js +15 -12
- package/src/shared/tenancyProfile.js +2 -2
- package/test/exportsContract.test.js +5 -0
- package/test/featureRuntime.test.js +5 -6
- package/test/packageMetadata.test.js +12 -0
- package/test/repositories.integration.test.js +1 -1
- package/test/requestedWorkspaceScope.test.js +24 -0
- package/test/roleLabels.test.js +29 -7
- package/test/support/invitationTransactions.js +35 -19
- package/test/workspaceActionSurfaces.test.js +0 -55
- package/test/workspaceAuthPolicyContextResolver.test.js +1 -1
- package/test/workspaceBootstrapContributor.test.js +7 -6
- package/test/workspaceCrud.test.js +23 -0
- package/test/workspaceInvitesRepository.test.js +9 -8
- package/test/workspaceMembersService.test.js +131 -28
- package/test/workspacePendingInvitationsResource.test.js +2 -2
- package/test/workspacePendingInvitationsService.test.js +69 -22
- package/test/workspaceService.test.js +58 -47
- package/test/workspaceSettingsActions.test.js +14 -7
- package/test/workspaceSettingsRepository.test.js +11 -0
- package/test/workspaceSettingsService.test.js +11 -9
- package/test/workspacesRepository.test.js +16 -16
- package/test/workspacesRouteRequestInputValidator.test.js +4 -4
- package/src/server/common/repositories/repositoryUtils.js +0 -43
- package/src/server/common/support/deepFreeze.js +0 -1
- package/src/server/common/support/resolveActionUser.js +0 -9
|
@@ -1,45 +1,28 @@
|
|
|
1
1
|
import { resolveInviteTokenHash } from "@jskit-ai/auth-core/server/inviteTokens";
|
|
2
2
|
import { encodeInviteTokenHash } from "@jskit-ai/auth-core/shared/inviteTokens";
|
|
3
|
+
import { returnJsonApiData } from "@jskit-ai/http-runtime/shared";
|
|
3
4
|
import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
|
|
4
|
-
import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
let acceptanceParticipant = null;
|
|
17
|
-
let registrationClosed = false;
|
|
18
|
-
|
|
5
|
+
import { normalizeLowerText, normalizeRecordId, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {object} dependencies
|
|
9
|
+
* @param {import("../types.js").WorkspaceInvitesRepository} dependencies.workspaceInvitesRepository
|
|
10
|
+
* @param {import("../types.js").WorkspaceMembershipsRepository} dependencies.workspaceMembershipsRepository
|
|
11
|
+
* @param {import("../types.js").WorkspaceRoleCatalog} dependencies.roleCatalog
|
|
12
|
+
* @param {import("../types.js").WorkspaceInviteAcceptanceParticipant} dependencies.acceptanceParticipant
|
|
13
|
+
*/
|
|
14
|
+
function createService({ workspaceInvitesRepository, workspaceMembershipsRepository, roleCatalog, acceptanceParticipant }) {
|
|
19
15
|
function roleLabelMetadata(roleSid) {
|
|
20
|
-
const role = roleCatalog
|
|
16
|
+
const role = roleCatalog.roles.find((entry) => entry.id === (normalizeLowerText(roleSid || "member") || "member"));
|
|
21
17
|
const label = typeof role?.label === "string" ? role.label.trim() : "";
|
|
22
18
|
return label ? { roleLabel: label } : {};
|
|
23
19
|
}
|
|
24
20
|
|
|
25
|
-
function registerAcceptanceParticipant(participant) {
|
|
26
|
-
if (typeof participant !== "function") {
|
|
27
|
-
throw new TypeError("Acceptance participant must be a function.");
|
|
28
|
-
}
|
|
29
|
-
if (acceptanceParticipant) {
|
|
30
|
-
throw new Error("An invitation acceptance participant is already registered.");
|
|
31
|
-
}
|
|
32
|
-
if (registrationClosed) {
|
|
33
|
-
throw new Error("Invitation acceptance participant registration is closed.");
|
|
34
|
-
}
|
|
35
|
-
acceptanceParticipant = participant;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
21
|
async function withInviteTransaction(user, token, options, work) {
|
|
22
|
+
// readable-check: allow required-collaborators - platform object: a Knex transaction, which acceptance and refusal open themselves
|
|
39
23
|
if (options.trx) {
|
|
40
24
|
throw new TypeError("Invitation acceptance/refusal owns its transaction; use the acceptance participant for related writes.");
|
|
41
25
|
}
|
|
42
|
-
registrationClosed = true;
|
|
43
26
|
// Locate outside the transaction to avoid a stale MySQL repeatable-read
|
|
44
27
|
// snapshot. The callback validates again under the workspace/invite locks.
|
|
45
28
|
const initial = await requirePendingInviteForUserByToken(user, token, options);
|
|
@@ -79,7 +62,7 @@ function createService({
|
|
|
79
62
|
return tokenHash;
|
|
80
63
|
}
|
|
81
64
|
|
|
82
|
-
function maskInviteEmail(email
|
|
65
|
+
function maskInviteEmail(email) {
|
|
83
66
|
const normalizedEmail = normalizeLowerText(email);
|
|
84
67
|
const [localPart = "", domain = ""] = normalizedEmail.split("@");
|
|
85
68
|
if (!localPart || !domain) {
|
|
@@ -89,7 +72,7 @@ function createService({
|
|
|
89
72
|
return `${visibleLocal}${"*".repeat(Math.max(localPart.length - visibleLocal.length, 1))}@${domain}`;
|
|
90
73
|
}
|
|
91
74
|
|
|
92
|
-
function resolvePublicInviteStatus(invite
|
|
75
|
+
function resolvePublicInviteStatus(invite) {
|
|
93
76
|
if (!invite) {
|
|
94
77
|
return "not_found";
|
|
95
78
|
}
|
|
@@ -103,7 +86,7 @@ function createService({
|
|
|
103
86
|
return "pending";
|
|
104
87
|
}
|
|
105
88
|
|
|
106
|
-
function mapResolvedInvite(invite
|
|
89
|
+
function mapResolvedInvite(invite, token) {
|
|
107
90
|
const status = resolvePublicInviteStatus(invite);
|
|
108
91
|
if (!invite) {
|
|
109
92
|
return {
|
|
@@ -141,7 +124,7 @@ function createService({
|
|
|
141
124
|
};
|
|
142
125
|
}
|
|
143
126
|
|
|
144
|
-
function mapPendingInvite(invite
|
|
127
|
+
function mapPendingInvite(invite) {
|
|
145
128
|
const id = normalizeRecordId(invite.id, { fallback: null });
|
|
146
129
|
const workspaceId = normalizeRecordId(invite.workspaceId, { fallback: null });
|
|
147
130
|
const tokenHash = normalizeText(invite.tokenHash);
|
|
@@ -175,7 +158,7 @@ function createService({
|
|
|
175
158
|
});
|
|
176
159
|
}
|
|
177
160
|
|
|
178
|
-
if (normalizeLowerText(invite.email) !== normalizeLowerText(actor
|
|
161
|
+
if (normalizeLowerText(invite.email) !== normalizeLowerText(actor.email)) {
|
|
179
162
|
throw new AppError(403, "Invitation email does not match authenticated user.", {
|
|
180
163
|
code: "workspaces.invite.emailMismatchUser"
|
|
181
164
|
});
|
|
@@ -189,26 +172,34 @@ function createService({
|
|
|
189
172
|
|
|
190
173
|
async function listPendingInvitesForUser(user, options = {}) {
|
|
191
174
|
const actor = requireAuthenticatedInviteUser(user);
|
|
192
|
-
const actorEmail = normalizeLowerText(actor
|
|
175
|
+
const actorEmail = normalizeLowerText(actor.email);
|
|
193
176
|
if (!actorEmail) {
|
|
194
177
|
return [];
|
|
195
178
|
}
|
|
196
179
|
|
|
197
180
|
const invites = await workspaceInvitesRepository.listPendingByEmail(actorEmail, options);
|
|
198
|
-
return invites.map(
|
|
181
|
+
return invites.map(mapPendingInvite).filter(Boolean);
|
|
199
182
|
}
|
|
200
183
|
|
|
201
|
-
|
|
184
|
+
// The bootstrap reads the pending invitations as they are; the action answers
|
|
185
|
+
// with them as JSON:API data.
|
|
186
|
+
async function listPendingInvitesResponse(user, options = {}) {
|
|
187
|
+
return returnJsonApiData({ pendingInvites: await listPendingInvitesForUser(user, options) });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function findInviteByToken(token, options = {}) {
|
|
202
191
|
const tokenHash = requireInviteTokenHash(token);
|
|
203
|
-
const invite =
|
|
204
|
-
? await workspaceInvitesRepository.findByTokenHashWithWorkspace(tokenHash, options)
|
|
205
|
-
: await workspaceInvitesRepository.findPendingByTokenHash(tokenHash, options);
|
|
192
|
+
const invite = await workspaceInvitesRepository.findByTokenHashWithWorkspace(tokenHash, options);
|
|
206
193
|
|
|
207
194
|
return mapResolvedInvite(invite, token);
|
|
208
195
|
}
|
|
209
196
|
|
|
210
|
-
async function
|
|
211
|
-
|
|
197
|
+
async function resolveInviteByToken(token, options = {}) {
|
|
198
|
+
return returnJsonApiData(await findInviteByToken(token, options));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function resolveInviteContextForAuth({ token, email }, options = {}) {
|
|
202
|
+
const resolved = await findInviteByToken(token, options);
|
|
212
203
|
if (resolved.status !== "pending") {
|
|
213
204
|
throw new AppError(409, "Invitation is not available.", { code: "workspaces.invite.notAvailable" });
|
|
214
205
|
}
|
|
@@ -229,15 +220,15 @@ function createService({
|
|
|
229
220
|
};
|
|
230
221
|
}
|
|
231
222
|
|
|
232
|
-
function requireWorkspaceIdFromInvite(invite, methodName
|
|
233
|
-
const workspaceId = normalizeRecordId(invite
|
|
223
|
+
function requireWorkspaceIdFromInvite(invite, methodName) {
|
|
224
|
+
const workspaceId = normalizeRecordId(invite.workspaceId, { fallback: null });
|
|
234
225
|
if (!workspaceId) {
|
|
235
226
|
throw new Error(`${methodName} expected invite workspace id.`);
|
|
236
227
|
}
|
|
237
228
|
return workspaceId;
|
|
238
229
|
}
|
|
239
230
|
|
|
240
|
-
async function resolveInviteActionInput(user, token, options
|
|
231
|
+
async function resolveInviteActionInput(user, token, options, methodName) {
|
|
241
232
|
const resolvedInvite = await requirePendingInviteForUserByToken(user, token, options);
|
|
242
233
|
if (resolvedInvite.invite.expiresAt && new Date(resolvedInvite.invite.expiresAt).getTime() < Date.now()) {
|
|
243
234
|
await workspaceInvitesRepository.revokeById(resolvedInvite.invite.id, options);
|
|
@@ -250,7 +241,7 @@ function createService({
|
|
|
250
241
|
};
|
|
251
242
|
}
|
|
252
243
|
|
|
253
|
-
async function acceptInviteByToken({ user, token }
|
|
244
|
+
async function acceptInviteByToken({ user, token }, options = {}) {
|
|
254
245
|
return withInviteTransaction(user, token, options, async (transactionOptions) => {
|
|
255
246
|
const resolved = await resolveInviteActionInput(
|
|
256
247
|
user,
|
|
@@ -268,19 +259,17 @@ function createService({
|
|
|
268
259
|
{ roleSid: resolvedInvite.invite.roleSid, status: "active" },
|
|
269
260
|
transactionOptions
|
|
270
261
|
);
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}, Object.freeze({ ...options, trx: transactionOptions.trx }));
|
|
277
|
-
}
|
|
262
|
+
// Repository locking options stay internal to this service.
|
|
263
|
+
await acceptanceParticipant({
|
|
264
|
+
invite: Object.freeze({ ...resolvedInvite.invite }),
|
|
265
|
+
user: Object.freeze({ ...resolvedInvite.user })
|
|
266
|
+
}, Object.freeze({ ...options, trx: transactionOptions.trx }));
|
|
278
267
|
await workspaceInvitesRepository.markAcceptedById(resolvedInvite.invite.id, transactionOptions);
|
|
279
268
|
return { decision: "accepted", workspaceId };
|
|
280
269
|
});
|
|
281
270
|
}
|
|
282
271
|
|
|
283
|
-
async function refuseInviteByToken({ user, token }
|
|
272
|
+
async function refuseInviteByToken({ user, token }, options = {}) {
|
|
284
273
|
return withInviteTransaction(user, token, options, async (transactionOptions) => {
|
|
285
274
|
const resolved = await resolveInviteActionInput(
|
|
286
275
|
user,
|
|
@@ -297,13 +286,21 @@ function createService({
|
|
|
297
286
|
});
|
|
298
287
|
}
|
|
299
288
|
|
|
289
|
+
// Accepts the invitation a token names when the user decided "accept", and refuses it otherwise.
|
|
290
|
+
async function redeemInviteByToken(user, { token, decision }, options = {}) {
|
|
291
|
+
return returnJsonApiData(decision === "accept"
|
|
292
|
+
? await acceptInviteByToken({ user, token }, options)
|
|
293
|
+
: await refuseInviteByToken({ user, token }, options));
|
|
294
|
+
}
|
|
295
|
+
|
|
300
296
|
return Object.freeze({
|
|
301
|
-
registerAcceptanceParticipant,
|
|
302
297
|
resolveInviteByToken,
|
|
303
298
|
resolveInviteContextForAuth,
|
|
304
299
|
listPendingInvitesForUser,
|
|
300
|
+
listPendingInvitesResponse,
|
|
305
301
|
acceptInviteByToken,
|
|
306
|
-
refuseInviteByToken
|
|
302
|
+
refuseInviteByToken,
|
|
303
|
+
redeemInviteByToken
|
|
307
304
|
});
|
|
308
305
|
}
|
|
309
306
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { composeSchemaDefinitions } from "@jskit-ai/kernel/shared/validators";
|
|
2
|
-
import { returnJsonApiData } from "@jskit-ai/http-runtime/shared";
|
|
3
2
|
import { workspaceSettingsResource } from "../../shared/resources/workspaceSettingsResource.js";
|
|
4
3
|
import { workspaceSlugParamsValidator } from "../common/validators/routeParamsValidator.js";
|
|
5
4
|
import { createWorkspaceEntityAndBootstrapEvents } from "../common/support/realtimeServiceEvents.js";
|
|
@@ -19,79 +18,60 @@ const workspaceSettingsUpdateInputValidator = composeSchemaDefinitions([
|
|
|
19
18
|
context: "workspaceSettingsActions.workspaceSettingsUpdateInputValidator"
|
|
20
19
|
});
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
context
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
return returnJsonApiData(response);
|
|
49
|
-
}
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
id: "workspace.settings.update",
|
|
53
|
-
version: 1,
|
|
54
|
-
kind: "command",
|
|
55
|
-
channels: ["api", "assistant_tool", "automation", "internal"],
|
|
56
|
-
surfaces: ["*"],
|
|
57
|
-
permission: {
|
|
58
|
-
require: "all",
|
|
59
|
-
permissions: ["workspace.settings.update"]
|
|
60
|
-
},
|
|
61
|
-
input: workspaceSettingsUpdateInputValidator,
|
|
62
|
-
output: null,
|
|
63
|
-
extensions: {
|
|
64
|
-
assistant: {
|
|
65
|
-
description: "Update the active workspace appearance and invitation settings.",
|
|
66
|
-
output: workspaceSettingsResource.operations.patch.output,
|
|
67
|
-
transformResult: (result) => result.value
|
|
68
|
-
}
|
|
69
|
-
},
|
|
70
|
-
idempotency: "optional",
|
|
71
|
-
events: WORKSPACE_SETTINGS_CHANGED_EVENTS,
|
|
72
|
-
async run(workspaceSettingsService, input, context) {
|
|
73
|
-
const { workspaceSlug, ...patch } = input;
|
|
74
|
-
const response = await workspaceSettingsService.updateWorkspaceSettings(
|
|
21
|
+
/** @param {{ workspaceSettingsService: import("../types.js").WorkspaceSettingsService }} dependencies */
|
|
22
|
+
function createWorkspaceSettingsActions({ workspaceSettingsService }) {
|
|
23
|
+
return Object.freeze([
|
|
24
|
+
Object.freeze({
|
|
25
|
+
id: "workspace.settings.read",
|
|
26
|
+
version: 1,
|
|
27
|
+
kind: "query",
|
|
28
|
+
channels: ["api", "automation", "internal"],
|
|
29
|
+
surfaces: ["*"],
|
|
30
|
+
permission: {
|
|
31
|
+
require: "any",
|
|
32
|
+
permissions: ["workspace.settings.view", "workspace.settings.update"]
|
|
33
|
+
},
|
|
34
|
+
input: workspaceSlugParamsValidator,
|
|
35
|
+
output: null,
|
|
36
|
+
extensions: {
|
|
37
|
+
assistant: {
|
|
38
|
+
description: "Read the active workspace appearance and invitation settings.",
|
|
39
|
+
output: workspaceSettingsResource.operations.view.output,
|
|
40
|
+
transformResult: (result) => result.value
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
execute: (input, context) => workspaceSettingsService.getWorkspaceSettings(
|
|
75
44
|
resolveWorkspace(context, input),
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
45
|
+
{ context }
|
|
46
|
+
)
|
|
47
|
+
}),
|
|
48
|
+
Object.freeze({
|
|
49
|
+
id: "workspace.settings.update",
|
|
50
|
+
version: 1,
|
|
51
|
+
kind: "command",
|
|
52
|
+
channels: ["api", "assistant_tool", "automation", "internal"],
|
|
53
|
+
surfaces: ["*"],
|
|
54
|
+
permission: {
|
|
55
|
+
require: "all",
|
|
56
|
+
permissions: ["workspace.settings.update"]
|
|
57
|
+
},
|
|
58
|
+
input: workspaceSettingsUpdateInputValidator,
|
|
59
|
+
output: null,
|
|
60
|
+
extensions: {
|
|
61
|
+
assistant: {
|
|
62
|
+
description: "Update the active workspace appearance and invitation settings.",
|
|
63
|
+
output: workspaceSettingsResource.operations.patch.output,
|
|
64
|
+
transformResult: (result) => result.value
|
|
79
65
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
return workspaceSettingsActionSpecifications.map(({ run, ...definition }) => Object.freeze({
|
|
90
|
-
...definition,
|
|
91
|
-
execute(input, context) {
|
|
92
|
-
return run(workspaceSettingsService, input, context);
|
|
93
|
-
}
|
|
94
|
-
}));
|
|
66
|
+
},
|
|
67
|
+
events: WORKSPACE_SETTINGS_CHANGED_EVENTS,
|
|
68
|
+
execute: (input, context) => workspaceSettingsService.updateWorkspaceSettings(
|
|
69
|
+
resolveWorkspace(context, input),
|
|
70
|
+
input,
|
|
71
|
+
{ context }
|
|
72
|
+
)
|
|
73
|
+
})
|
|
74
|
+
]);
|
|
95
75
|
}
|
|
96
76
|
|
|
97
|
-
export {
|
|
77
|
+
export { createWorkspaceSettingsActions };
|
|
@@ -1,17 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
normalizeRecordId,
|
|
3
|
-
isDuplicateEntryError,
|
|
4
|
-
retryDeadlocks
|
|
5
|
-
} from "../common/repositories/repositoryUtils.js";
|
|
1
|
+
import { isLostDuplicateInsertRace, resolveTransaction, retryDeadlocks } from "@jskit-ai/database-runtime/shared";
|
|
6
2
|
import {
|
|
7
3
|
createJsonRestContext,
|
|
8
4
|
extractJsonRestCollectionRows
|
|
9
5
|
} from "@jskit-ai/json-rest-api-core/server/jsonRestApiHost";
|
|
10
6
|
import { pickOwnProperties } from "@jskit-ai/kernel/shared/support";
|
|
7
|
+
import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
|
|
11
8
|
import { WORKSPACE_SETTINGS_FIELD_KEYS } from "../../shared/resources/workspaceSettingsResource.js";
|
|
12
9
|
import { resolveWorkspaceThemePalettes } from "../../shared/settings.js";
|
|
13
10
|
|
|
14
|
-
function
|
|
11
|
+
function buildDefaultWorkspaceSettingsCreatePayload(workspaceId, defaultInvitesEnabled, defaultColorSettings) {
|
|
15
12
|
const palettes = resolveWorkspaceThemePalettes(defaultColorSettings);
|
|
16
13
|
|
|
17
14
|
return {
|
|
@@ -28,10 +25,13 @@ function createDefaultWorkspaceSettingsCreatePayload(workspaceId, defaultInvites
|
|
|
28
25
|
};
|
|
29
26
|
}
|
|
30
27
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
28
|
+
/**
|
|
29
|
+
* @param {object} dependencies
|
|
30
|
+
* @param {import("@jskit-ai/json-rest-api-core/server/types").RuntimeJsonRestApi} dependencies.api
|
|
31
|
+
* @param {boolean} dependencies.defaultInvitesEnabled
|
|
32
|
+
* @param {object} dependencies.defaultColorSettings The configured colors that replace the theme's.
|
|
33
|
+
*/
|
|
34
|
+
function createRepository({ api, defaultInvitesEnabled, defaultColorSettings }) {
|
|
35
35
|
const withTransaction = (work) => retryDeadlocks(() => api.transaction(work));
|
|
36
36
|
|
|
37
37
|
async function queryFirst(filters = {}, options = {}) {
|
|
@@ -41,10 +41,10 @@ function createRepository({ api, defaultInvitesEnabled = true, defaultColorSetti
|
|
|
41
41
|
queryParams: {
|
|
42
42
|
filters
|
|
43
43
|
},
|
|
44
|
-
transaction: options
|
|
44
|
+
transaction: resolveTransaction(options),
|
|
45
45
|
format: "plain"
|
|
46
46
|
},
|
|
47
|
-
createJsonRestContext(options
|
|
47
|
+
createJsonRestContext(options.context)
|
|
48
48
|
)
|
|
49
49
|
);
|
|
50
50
|
|
|
@@ -74,19 +74,19 @@ function createRepository({ api, defaultInvitesEnabled = true, defaultColorSetti
|
|
|
74
74
|
try {
|
|
75
75
|
await api.resources.workspaceSettings.post(
|
|
76
76
|
{
|
|
77
|
-
data:
|
|
77
|
+
data: buildDefaultWorkspaceSettingsCreatePayload(
|
|
78
78
|
normalizedWorkspaceId,
|
|
79
|
-
defaultInvitesEnabled
|
|
79
|
+
defaultInvitesEnabled,
|
|
80
80
|
defaultColorSettings
|
|
81
81
|
),
|
|
82
82
|
format: "plain",
|
|
83
83
|
returning: "full",
|
|
84
|
-
transaction: options
|
|
84
|
+
transaction: resolveTransaction(options)
|
|
85
85
|
},
|
|
86
|
-
createJsonRestContext(options
|
|
86
|
+
createJsonRestContext(options.context)
|
|
87
87
|
);
|
|
88
88
|
} catch (error) {
|
|
89
|
-
if (
|
|
89
|
+
if (!isLostDuplicateInsertRace(error, options)) {
|
|
90
90
|
throw error;
|
|
91
91
|
}
|
|
92
92
|
}
|
|
@@ -94,7 +94,7 @@ function createRepository({ api, defaultInvitesEnabled = true, defaultColorSetti
|
|
|
94
94
|
return findByWorkspaceId(normalizedWorkspaceId, options);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
async function updateSettingsByWorkspaceId(workspaceId, patch
|
|
97
|
+
async function updateSettingsByWorkspaceId(workspaceId, patch, options = {}) {
|
|
98
98
|
const normalizedWorkspaceId = normalizeRecordId(workspaceId, { fallback: null });
|
|
99
99
|
if (!normalizedWorkspaceId) {
|
|
100
100
|
throw new TypeError("workspaceSettingsRepository.updateSettingsByWorkspaceId requires a valid workspace id.");
|
|
@@ -117,9 +117,9 @@ function createRepository({ api, defaultInvitesEnabled = true, defaultColorSetti
|
|
|
117
117
|
},
|
|
118
118
|
format: "plain",
|
|
119
119
|
returning: "full",
|
|
120
|
-
transaction: options
|
|
120
|
+
transaction: resolveTransaction(options)
|
|
121
121
|
},
|
|
122
|
-
createJsonRestContext(options
|
|
122
|
+
createJsonRestContext(options.context)
|
|
123
123
|
);
|
|
124
124
|
|
|
125
125
|
return findByWorkspaceId(normalizedWorkspaceId, options);
|
package/src/server/workspaceSettings/{bootWorkspaceSettings.js → workspaceSettingsRoutes.js}
RENAMED
|
@@ -1,28 +1,16 @@
|
|
|
1
1
|
import { createJsonApiResourceRouteContract } from "@jskit-ai/http-runtime/shared/validators/jsonApiRouteTransport";
|
|
2
2
|
import { WORKSPACE_SETTINGS_TRANSPORT } from "../../shared/jsonApiTransports.js";
|
|
3
3
|
import { workspaceSettingsResource } from "../../shared/resources/workspaceSettingsResource.js";
|
|
4
|
-
import {
|
|
4
|
+
import { resolveWorkspaceRecordId } from "../common/support/workspaceRecordId.js";
|
|
5
|
+
import { resolveWorkspaceRoutePath } from "../support/workspaceRoutePaths.js";
|
|
5
6
|
import { workspaceSlugParamsValidator } from "../common/validators/routeParamsValidator.js";
|
|
6
7
|
import { resolveDefaultWorkspaceRouteSurfaceIdFromAppConfig } from "../support/workspaceActionSurfaces.js";
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const workspaceSlug = context?.request?.params?.workspaceSlug;
|
|
15
|
-
if (workspaceSlug != null && String(workspaceSlug).trim()) {
|
|
16
|
-
return workspaceSlug;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
throw new Error("Workspace settings JSON:API response requires workspace id.");
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function registerWorkspaceSettingsRoutes(router, { config = {} } = {}) {
|
|
23
|
-
if (!router || typeof router.register !== "function") {
|
|
24
|
-
throw new TypeError("registerWorkspaceSettingsRoutes requires router.register().");
|
|
25
|
-
}
|
|
9
|
+
/**
|
|
10
|
+
* @param {import("@jskit-ai/kernel/server/types").RuntimeRouter} router
|
|
11
|
+
* @param {{ config: import("@jskit-ai/kernel/server/types").RuntimeConfig }} options
|
|
12
|
+
*/
|
|
13
|
+
function registerWorkspaceSettingsRoutes(router, { config }) {
|
|
26
14
|
const workspaceRouteSurfaceId = resolveDefaultWorkspaceRouteSurfaceIdFromAppConfig(config);
|
|
27
15
|
|
|
28
16
|
router.register(
|
|
@@ -41,7 +29,7 @@ function registerWorkspaceSettingsRoutes(router, { config = {} } = {}) {
|
|
|
41
29
|
...WORKSPACE_SETTINGS_TRANSPORT,
|
|
42
30
|
output: workspaceSettingsResource.operations.view.output,
|
|
43
31
|
outputKind: "record",
|
|
44
|
-
getRecordId:
|
|
32
|
+
getRecordId: resolveWorkspaceRecordId
|
|
45
33
|
})
|
|
46
34
|
},
|
|
47
35
|
async function (request, reply) {
|
|
@@ -72,7 +60,7 @@ function registerWorkspaceSettingsRoutes(router, { config = {} } = {}) {
|
|
|
72
60
|
body: workspaceSettingsResource.operations.patch.body,
|
|
73
61
|
output: workspaceSettingsResource.operations.patch.output,
|
|
74
62
|
outputKind: "record",
|
|
75
|
-
getRecordId:
|
|
63
|
+
getRecordId: resolveWorkspaceRecordId,
|
|
76
64
|
includeValidation400: true
|
|
77
65
|
})
|
|
78
66
|
},
|
|
@@ -1,23 +1,17 @@
|
|
|
1
1
|
import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
-
import { normalizeObjectInput } from "@jskit-ai/kernel/shared/validators/inputNormalization";
|
|
3
2
|
import { pickOwnProperties } from "@jskit-ai/kernel/shared/support";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
} from "../../shared/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
const resolvedRoleCatalog = roleCatalog && typeof roleCatalog === "object" ? roleCatalog : createWorkspaceRoleCatalog();
|
|
18
|
-
const invitesAvailable = workspaceInvitationsEnabled === true;
|
|
19
|
-
|
|
20
|
-
async function getWorkspaceSettings(workspace, options = {}) {
|
|
3
|
+
import { returnJsonApiData } from "@jskit-ai/http-runtime/shared";
|
|
4
|
+
import { WORKSPACE_SETTINGS_FIELD_KEYS } from "../../shared/resources/workspaceSettingsResource.js";
|
|
5
|
+
import { cloneWorkspaceRoleCatalog } from "../../shared/roles.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {object} dependencies
|
|
9
|
+
* @param {import("../types.js").WorkspaceSettingsRepository} dependencies.workspaceSettingsRepository
|
|
10
|
+
* @param {boolean} dependencies.workspaceInvitationsEnabled
|
|
11
|
+
* @param {import("../types.js").WorkspaceRoleCatalog} dependencies.roleCatalog
|
|
12
|
+
*/
|
|
13
|
+
function createService({ workspaceSettingsRepository, workspaceInvitationsEnabled, roleCatalog }) {
|
|
14
|
+
async function readWorkspaceSettings(workspace, options = {}) {
|
|
21
15
|
const settingsRecord = await workspaceSettingsRepository.ensureForWorkspaceId(workspace.id, {
|
|
22
16
|
...options,
|
|
23
17
|
workspace
|
|
@@ -26,10 +20,10 @@ function createService({
|
|
|
26
20
|
for (const fieldKey of WORKSPACE_SETTINGS_FIELD_KEYS) {
|
|
27
21
|
settings[fieldKey] = settingsRecord[fieldKey];
|
|
28
22
|
}
|
|
29
|
-
const invitesEnabled =
|
|
23
|
+
const invitesEnabled = workspaceInvitationsEnabled && settings.invitesEnabled !== false;
|
|
30
24
|
settings.invitesEnabled = invitesEnabled;
|
|
31
|
-
settings.invitesAvailable =
|
|
32
|
-
settings.invitesEffective =
|
|
25
|
+
settings.invitesAvailable = workspaceInvitationsEnabled;
|
|
26
|
+
settings.invitesEffective = invitesEnabled;
|
|
33
27
|
|
|
34
28
|
return {
|
|
35
29
|
workspace: {
|
|
@@ -38,13 +32,17 @@ function createService({
|
|
|
38
32
|
ownerUserId: normalizeRecordId(workspace.ownerUserId, { fallback: "" })
|
|
39
33
|
},
|
|
40
34
|
settings,
|
|
41
|
-
roleCatalog: cloneWorkspaceRoleCatalog(
|
|
35
|
+
roleCatalog: cloneWorkspaceRoleCatalog(roleCatalog)
|
|
42
36
|
};
|
|
43
37
|
}
|
|
44
38
|
|
|
45
|
-
async function
|
|
46
|
-
|
|
47
|
-
|
|
39
|
+
async function getWorkspaceSettings(workspace, options = {}) {
|
|
40
|
+
return returnJsonApiData(await readWorkspaceSettings(workspace, options));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The action's validated input; only the settings fields go to the repository. */
|
|
44
|
+
async function updateWorkspaceSettings(workspace, input, options = {}) {
|
|
45
|
+
const settingsPatch = pickOwnProperties(input, WORKSPACE_SETTINGS_FIELD_KEYS);
|
|
48
46
|
|
|
49
47
|
if (Object.keys(settingsPatch).length > 0) {
|
|
50
48
|
await workspaceSettingsRepository.updateSettingsByWorkspaceId(workspace.id, settingsPatch, {
|
|
@@ -53,7 +51,7 @@ function createService({
|
|
|
53
51
|
});
|
|
54
52
|
}
|
|
55
53
|
|
|
56
|
-
return
|
|
54
|
+
return returnJsonApiData(await readWorkspaceSettings(workspace, options));
|
|
57
55
|
}
|
|
58
56
|
|
|
59
57
|
return Object.freeze({
|