@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,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveRequest } from "@jskit-ai/kernel/server/actions/actionContributorHelpers";
|
|
2
2
|
import { normalizeObject } from "@jskit-ai/kernel/shared/support/normalize";
|
|
3
3
|
import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
|
|
4
4
|
import {
|
|
@@ -7,91 +7,81 @@ import {
|
|
|
7
7
|
ROUTE_VISIBILITY_WORKSPACE,
|
|
8
8
|
ROUTE_VISIBILITY_WORKSPACE_USER
|
|
9
9
|
} from "@jskit-ai/kernel/shared/support/visibility";
|
|
10
|
-
import { resolveActionUser } from "
|
|
10
|
+
import { resolveActionUser } from "@jskit-ai/users-core/server/resolveActionUser";
|
|
11
|
+
|
|
11
12
|
const WORKSPACE_VISIBILITY_ACTION_CONTEXT_SET = new Set([
|
|
12
13
|
ROUTE_VISIBILITY_WORKSPACE,
|
|
13
14
|
ROUTE_VISIBILITY_WORKSPACE_USER
|
|
14
15
|
]);
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (!surfaceId) {
|
|
23
|
-
continue;
|
|
24
|
-
}
|
|
25
|
-
normalized.add(surfaceId);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return normalized;
|
|
29
|
-
}
|
|
30
|
-
|
|
17
|
+
/**
|
|
18
|
+
* @param {object} dependencies
|
|
19
|
+
* @param {import("../../types.js").WorkspaceDirectoryService} dependencies.workspaceService
|
|
20
|
+
* @param {string[]} dependencies.workspaceMembershipOptionalSurfaceIds The surfaces' normalized ids.
|
|
21
|
+
* @param {string[]} dependencies.workspaceSurfaceIds The surfaces' normalized ids.
|
|
22
|
+
*/
|
|
31
23
|
function createWorkspaceActionContextContributor({
|
|
32
24
|
workspaceService,
|
|
33
|
-
workspaceMembershipOptionalSurfaceIds
|
|
34
|
-
workspaceSurfaceIds
|
|
35
|
-
}
|
|
25
|
+
workspaceMembershipOptionalSurfaceIds,
|
|
26
|
+
workspaceSurfaceIds
|
|
27
|
+
}) {
|
|
36
28
|
const contributorId = "workspaces.context";
|
|
37
|
-
const workspaceMembershipOptionalSurfaceIdSet =
|
|
38
|
-
|
|
39
|
-
);
|
|
40
|
-
const workspaceSurfaceIdSet = normalizeWorkspaceSurfaceIds(workspaceSurfaceIds);
|
|
29
|
+
const workspaceMembershipOptionalSurfaceIdSet = new Set(workspaceMembershipOptionalSurfaceIds);
|
|
30
|
+
const workspaceSurfaceIdSet = new Set(workspaceSurfaceIds);
|
|
41
31
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const payload = normalizeObject(input);
|
|
48
|
-
if (!Object.hasOwn(payload, "workspaceSlug")) {
|
|
49
|
-
return {};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const request = context?.requestMeta?.request || null;
|
|
53
|
-
const actionSurfaces = Array.isArray(definition?.surfaces) ? definition.surfaces : [];
|
|
54
|
-
const hasWorkspaceActionSurface = actionSurfaces.some((surfaceId) => workspaceSurfaceIdSet.has(surfaceId));
|
|
55
|
-
const routeSurfaceId = normalizeSurfaceId(request?.routeOptions?.config?.surface);
|
|
56
|
-
const activeSurfaceId = routeSurfaceId || normalizeSurfaceId(context?.surface);
|
|
57
|
-
const hasWorkspaceSurface = workspaceSurfaceIdSet.has(routeSurfaceId);
|
|
58
|
-
const routeVisibilityInput =
|
|
59
|
-
context?.routeVisibility ?? request?.routeOptions?.config?.visibility ?? ROUTE_VISIBILITY_PUBLIC;
|
|
60
|
-
const routeVisibility = checkRouteVisibility(routeVisibilityInput);
|
|
61
|
-
const hasWorkspaceRouteVisibility = WORKSPACE_VISIBILITY_ACTION_CONTEXT_SET.has(routeVisibility);
|
|
62
|
-
if (!hasWorkspaceActionSurface && !hasWorkspaceRouteVisibility && !hasWorkspaceSurface) {
|
|
63
|
-
return {};
|
|
64
|
-
}
|
|
32
|
+
async function contribute({ definition = null, input, context } = {}) {
|
|
33
|
+
const payload = normalizeObject(input);
|
|
34
|
+
if (!Object.hasOwn(payload, "workspaceSlug")) {
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
65
37
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
38
|
+
const request = resolveRequest(context);
|
|
39
|
+
const actionSurfaces = Array.isArray(definition?.surfaces) ? definition.surfaces : [];
|
|
40
|
+
const hasWorkspaceActionSurface = actionSurfaces.some((surfaceId) => workspaceSurfaceIdSet.has(surfaceId));
|
|
41
|
+
const routeSurfaceId = normalizeSurfaceId(request?.routeOptions?.config?.surface);
|
|
42
|
+
const activeSurfaceId = routeSurfaceId || normalizeSurfaceId(context?.surface);
|
|
43
|
+
const hasWorkspaceSurface = workspaceSurfaceIdSet.has(routeSurfaceId);
|
|
44
|
+
const routeVisibilityInput =
|
|
45
|
+
context?.routeVisibility ?? request?.routeOptions?.config?.visibility ?? ROUTE_VISIBILITY_PUBLIC;
|
|
46
|
+
const routeVisibility = checkRouteVisibility(routeVisibilityInput);
|
|
47
|
+
const hasWorkspaceRouteVisibility = WORKSPACE_VISIBILITY_ACTION_CONTEXT_SET.has(routeVisibility);
|
|
48
|
+
if (!hasWorkspaceActionSurface && !hasWorkspaceRouteVisibility && !hasWorkspaceSurface) {
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
70
51
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
);
|
|
52
|
+
const resolveOptions = { request };
|
|
53
|
+
if (workspaceMembershipOptionalSurfaceIdSet.has(activeSurfaceId)) {
|
|
54
|
+
resolveOptions.requireMembership = false;
|
|
55
|
+
}
|
|
76
56
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
57
|
+
const resolvedWorkspaceContext = await workspaceService.resolveWorkspaceContextForUserBySlug(
|
|
58
|
+
resolveActionUser(context, payload),
|
|
59
|
+
payload.workspaceSlug,
|
|
60
|
+
resolveOptions
|
|
61
|
+
);
|
|
82
62
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (!context?.membership) {
|
|
87
|
-
contribution.membership = resolvedWorkspaceContext.membership;
|
|
88
|
-
}
|
|
89
|
-
if (!Array.isArray(context?.permissions) || context.permissions.length < 1) {
|
|
90
|
-
contribution.permissions = resolvedWorkspaceContext.permissions;
|
|
63
|
+
const contribution = {
|
|
64
|
+
requestMeta: {
|
|
65
|
+
resolvedWorkspaceContext
|
|
91
66
|
}
|
|
67
|
+
};
|
|
92
68
|
|
|
93
|
-
|
|
69
|
+
if (!context?.workspace) {
|
|
70
|
+
contribution.workspace = resolvedWorkspaceContext.workspace;
|
|
71
|
+
}
|
|
72
|
+
if (!context?.membership) {
|
|
73
|
+
contribution.membership = resolvedWorkspaceContext.membership;
|
|
94
74
|
}
|
|
75
|
+
if (!Array.isArray(context?.permissions) || context.permissions.length < 1) {
|
|
76
|
+
contribution.permissions = resolvedWorkspaceContext.permissions;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return contribution;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return Object.freeze({
|
|
83
|
+
contributorId,
|
|
84
|
+
contribute
|
|
95
85
|
});
|
|
96
86
|
}
|
|
97
87
|
|
|
@@ -1,21 +1,13 @@
|
|
|
1
1
|
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
2
|
import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const membershipOptionalSurfaceIds = new Set(
|
|
15
|
-
(Array.isArray(workspaceMembershipOptionalSurfaceIds) ? workspaceMembershipOptionalSurfaceIds : [])
|
|
16
|
-
.map((entry) => normalizeSurfaceId(entry))
|
|
17
|
-
.filter(Boolean)
|
|
18
|
-
);
|
|
4
|
+
/**
|
|
5
|
+
* @param {object} dependencies
|
|
6
|
+
* @param {import("../../types.js").WorkspaceDirectoryService} dependencies.workspaceService
|
|
7
|
+
* @param {string[]} dependencies.workspaceMembershipOptionalSurfaceIds The surfaces' normalized ids.
|
|
8
|
+
*/
|
|
9
|
+
function createWorkspaceAuthPolicyContextResolver({ workspaceService, workspaceMembershipOptionalSurfaceIds }) {
|
|
10
|
+
const membershipOptionalSurfaceIds = new Set(workspaceMembershipOptionalSurfaceIds);
|
|
19
11
|
|
|
20
12
|
return async function resolveWorkspaceAuthPolicyContext({ request, actor, meta } = {}) {
|
|
21
13
|
const contextPolicy = normalizeText(meta?.contextPolicy || "none").toLowerCase() || "none";
|
|
@@ -29,26 +21,19 @@ function createWorkspaceAuthPolicyContextResolver({
|
|
|
29
21
|
return {};
|
|
30
22
|
}
|
|
31
23
|
|
|
32
|
-
const resolveOptions = { request };
|
|
33
24
|
const activeSurfaceId = normalizeSurfaceId(request?.routeOptions?.config?.surface);
|
|
34
|
-
|
|
25
|
+
const requireMembership = !(
|
|
35
26
|
(contextPolicy === "optional" && !permission) ||
|
|
36
27
|
membershipOptionalSurfaceIds.has(activeSurfaceId)
|
|
37
|
-
)
|
|
38
|
-
resolveOptions.requireMembership = false;
|
|
39
|
-
}
|
|
28
|
+
);
|
|
40
29
|
|
|
41
|
-
const
|
|
30
|
+
const { workspace, membership, permissions } = await workspaceService.resolveWorkspaceContextForUserBySlug(
|
|
42
31
|
actor,
|
|
43
32
|
workspaceSlug,
|
|
44
|
-
|
|
33
|
+
{ requireMembership }
|
|
45
34
|
);
|
|
46
35
|
|
|
47
|
-
return {
|
|
48
|
-
workspace: resolvedWorkspaceContext?.workspace || null,
|
|
49
|
-
membership: resolvedWorkspaceContext?.membership || null,
|
|
50
|
-
permissions: Array.isArray(resolvedWorkspaceContext?.permissions) ? resolvedWorkspaceContext.permissions : []
|
|
51
|
-
};
|
|
36
|
+
return { workspace, membership, permissions };
|
|
52
37
|
};
|
|
53
38
|
}
|
|
54
39
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { normalizeRecordId, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
2
|
import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
|
|
3
|
+
import { resolveActor, resolveActorId } from "@jskit-ai/kernel/server/actions/actionContributorHelpers";
|
|
3
4
|
|
|
4
5
|
function buildVisibilityContribution({ visibility, scopeOwnerId = null, userId = null } = {}) {
|
|
5
6
|
const requiresActorScope = visibility === "workspace_user";
|
|
@@ -18,78 +19,62 @@ function buildVisibilityContribution({ visibility, scopeOwnerId = null, userId =
|
|
|
18
19
|
return contribution;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const membershipOptionalSurfaceIds = new Set(
|
|
30
|
-
(Array.isArray(workspaceMembershipOptionalSurfaceIds) ? workspaceMembershipOptionalSurfaceIds : [])
|
|
31
|
-
.map((entry) => normalizeSurfaceId(entry))
|
|
32
|
-
.filter(Boolean)
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
return Object.freeze({
|
|
36
|
-
resolverId: "workspaces.visibility",
|
|
37
|
-
async resolve({ visibility, context, request, input } = {}) {
|
|
38
|
-
if (visibility !== "workspace" && visibility !== "workspace_user") {
|
|
39
|
-
return {};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const actor = context?.actor || request?.user || null;
|
|
43
|
-
const userId = normalizeOpaqueId(actor?.id);
|
|
44
|
-
const workspace =
|
|
45
|
-
context?.workspace || context?.requestMeta?.resolvedWorkspaceContext?.workspace || request?.workspace || null;
|
|
46
|
-
const scopeOwnerId = normalizeRecordId(workspace?.id, { fallback: null });
|
|
47
|
-
if (!scopeOwnerId) {
|
|
48
|
-
const workspaceSlug = normalizeText(input?.workspaceSlug).toLowerCase();
|
|
22
|
+
/**
|
|
23
|
+
* @param {object} dependencies
|
|
24
|
+
* @param {import("../../types.js").WorkspaceDirectoryService} dependencies.workspaceService
|
|
25
|
+
* @param {string[]} dependencies.workspaceMembershipOptionalSurfaceIds The surfaces' normalized ids.
|
|
26
|
+
*/
|
|
27
|
+
function createWorkspaceRouteVisibilityResolver({ workspaceService, workspaceMembershipOptionalSurfaceIds }) {
|
|
28
|
+
const membershipOptionalSurfaceIds = new Set(workspaceMembershipOptionalSurfaceIds);
|
|
49
29
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
userId
|
|
55
|
-
})
|
|
56
|
-
: {};
|
|
57
|
-
}
|
|
30
|
+
async function resolve({ visibility, context, request, input } = {}) {
|
|
31
|
+
if (visibility !== "workspace" && visibility !== "workspace_user") {
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
58
34
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
request,
|
|
67
|
-
...(membershipOptionalSurfaceIds.has(activeSurfaceId) ? { requireMembership: false } : {})
|
|
68
|
-
}
|
|
69
|
-
);
|
|
70
|
-
const resolvedWorkspaceOwnerId = normalizeRecordId(resolvedWorkspaceContext?.workspace?.id, { fallback: null });
|
|
71
|
-
if (!resolvedWorkspaceOwnerId) {
|
|
72
|
-
return visibility === "workspace_user"
|
|
73
|
-
? buildVisibilityContribution({
|
|
74
|
-
visibility,
|
|
75
|
-
userId
|
|
76
|
-
})
|
|
77
|
-
: {};
|
|
78
|
-
}
|
|
35
|
+
const actor = resolveActor(context, request);
|
|
36
|
+
const userId = resolveActorId(context, request);
|
|
37
|
+
const workspace =
|
|
38
|
+
context?.workspace || context?.requestMeta?.resolvedWorkspaceContext?.workspace || request?.workspace || null;
|
|
39
|
+
const scopeOwnerId = normalizeRecordId(workspace?.id, { fallback: null });
|
|
40
|
+
if (!scopeOwnerId) {
|
|
41
|
+
const workspaceSlug = normalizeText(input?.workspaceSlug).toLowerCase();
|
|
79
42
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
43
|
+
if (!workspaceSlug || !actor) {
|
|
44
|
+
return visibility === "workspace_user"
|
|
45
|
+
? buildVisibilityContribution({
|
|
46
|
+
visibility,
|
|
47
|
+
userId
|
|
48
|
+
})
|
|
49
|
+
: {};
|
|
85
50
|
}
|
|
86
51
|
|
|
52
|
+
const activeSurfaceId = normalizeSurfaceId(
|
|
53
|
+
request?.routeOptions?.config?.surface || context?.surface
|
|
54
|
+
);
|
|
55
|
+
const resolvedWorkspaceContext = await workspaceService.resolveWorkspaceContextForUserBySlug(
|
|
56
|
+
actor,
|
|
57
|
+
workspaceSlug,
|
|
58
|
+
{ requireMembership: !membershipOptionalSurfaceIds.has(activeSurfaceId) }
|
|
59
|
+
);
|
|
60
|
+
|
|
87
61
|
return buildVisibilityContribution({
|
|
88
62
|
visibility,
|
|
89
|
-
scopeOwnerId,
|
|
63
|
+
scopeOwnerId: normalizeRecordId(resolvedWorkspaceContext.workspace.id),
|
|
90
64
|
userId
|
|
91
65
|
});
|
|
92
66
|
}
|
|
67
|
+
|
|
68
|
+
return buildVisibilityContribution({
|
|
69
|
+
visibility,
|
|
70
|
+
scopeOwnerId,
|
|
71
|
+
userId
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return Object.freeze({
|
|
76
|
+
resolverId: "workspaces.visibility",
|
|
77
|
+
resolve
|
|
93
78
|
});
|
|
94
79
|
}
|
|
95
80
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
|
|
1
|
+
import { normalizeLowerText, normalizeRecordId, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
+
import { resolveWorkspaceThemePalettes } from "../../../shared/settings.js";
|
|
4
3
|
|
|
5
4
|
function mapWorkspaceSummary(workspace, membership, roleCatalog = null) {
|
|
6
5
|
const roleSid = normalizeLowerText(membership?.roleSid || "member") || "member";
|
|
@@ -16,11 +15,14 @@ function mapWorkspaceSummary(workspace, membership, roleCatalog = null) {
|
|
|
16
15
|
};
|
|
17
16
|
}
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
/**
|
|
19
|
+
* A workspace's settings as the bootstrap shows them.
|
|
20
|
+
* @param {object} workspaceSettings The workspace's settings record.
|
|
21
|
+
* @param {{ workspaceInvitationsEnabled: boolean }} policy Whether the app allows invitations.
|
|
22
|
+
*/
|
|
23
|
+
function mapWorkspaceSettingsPublic(workspaceSettings, { workspaceInvitationsEnabled }) {
|
|
24
|
+
const invitesEnabled = workspaceInvitationsEnabled && workspaceSettings.invitesEnabled !== false;
|
|
25
|
+
const themePalettes = resolveWorkspaceThemePalettes(workspaceSettings);
|
|
24
26
|
|
|
25
27
|
return {
|
|
26
28
|
lightPrimaryColor: themePalettes.light.color,
|
|
@@ -32,8 +34,8 @@ function mapWorkspaceSettingsPublic(workspaceSettings, { workspaceInvitationsEna
|
|
|
32
34
|
darkSurfaceColor: themePalettes.dark.surfaceColor,
|
|
33
35
|
darkSurfaceVariantColor: themePalettes.dark.surfaceVariantColor,
|
|
34
36
|
invitesEnabled,
|
|
35
|
-
invitesAvailable,
|
|
36
|
-
invitesEffective:
|
|
37
|
+
invitesAvailable: workspaceInvitationsEnabled,
|
|
38
|
+
invitesEffective: invitesEnabled
|
|
37
39
|
};
|
|
38
40
|
}
|
|
39
41
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
|
|
2
1
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
isLostDuplicateInsertRace,
|
|
3
|
+
joinOrOpenTransaction,
|
|
5
4
|
normalizeDbRecordId,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
toIsoString
|
|
9
|
-
|
|
10
|
-
} from "
|
|
5
|
+
resolveTransaction,
|
|
6
|
+
retryDeadlocks,
|
|
7
|
+
toIsoString
|
|
8
|
+
} from "@jskit-ai/database-runtime/shared";
|
|
9
|
+
import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
|
|
10
|
+
import { normalizeLowerText, normalizeRecordId, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
11
11
|
import {
|
|
12
12
|
createJsonRestContext,
|
|
13
13
|
extractJsonRestCollectionRows
|
|
@@ -71,21 +71,19 @@ function normalizeInviteWithWorkspace(payload = {}) {
|
|
|
71
71
|
|
|
72
72
|
return {
|
|
73
73
|
...invite,
|
|
74
|
-
workspaceSlug: payload
|
|
75
|
-
workspaceName: payload
|
|
76
|
-
workspaceAvatarUrl: payload
|
|
74
|
+
workspaceSlug: payload.workspace?.slug ? normalizeText(payload.workspace.slug) : undefined,
|
|
75
|
+
workspaceName: payload.workspace?.name ? normalizeText(payload.workspace.name) : undefined,
|
|
76
|
+
workspaceAvatarUrl: payload.workspace?.avatarUrl ? normalizeText(payload.workspace.avatarUrl) : undefined
|
|
77
77
|
};
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
throw new TypeError("workspaceInvitesRepository requires json-rest-api workspaceInvites resource.");
|
|
83
|
-
}
|
|
84
|
-
|
|
80
|
+
/** @param {{ api: import("@jskit-ai/json-rest-api-core/server/types").RuntimeJsonRestApi }} dependencies */
|
|
81
|
+
function createRepository({ api }) {
|
|
85
82
|
const withTransaction = (work) => retryDeadlocks(() => api.transaction(work));
|
|
86
83
|
|
|
87
84
|
async function queryInvites(filters = {}, options = {}, { includeWorkspace = false } = {}) {
|
|
88
85
|
if (options.lock === true) {
|
|
86
|
+
// readable-check: allow method-probe - platform object: a Knex transaction, which a locking read needs and options may omit
|
|
89
87
|
if (typeof options.trx !== "function" || includeWorkspace) {
|
|
90
88
|
throw new TypeError("Locking invites requires a transaction and no relationship includes.");
|
|
91
89
|
}
|
|
@@ -104,6 +102,7 @@ function createRepository({ api } = {}) {
|
|
|
104
102
|
return [fields[key], value];
|
|
105
103
|
}));
|
|
106
104
|
// Read PostgreSQL's UTC timestamps as text to avoid the driver's local-time conversion.
|
|
105
|
+
// readable-check: allow method-probe - platform object: a Knex transaction, whose client config names its dialect
|
|
107
106
|
const dateColumn = (name) => options.trx.client?.config?.client === "pg"
|
|
108
107
|
? options.trx.raw("CAST(?? AS TEXT)", [name])
|
|
109
108
|
: name;
|
|
@@ -129,16 +128,17 @@ function createRepository({ api } = {}) {
|
|
|
129
128
|
filters,
|
|
130
129
|
...(includeWorkspace ? { include: ["workspace"] } : {})
|
|
131
130
|
},
|
|
132
|
-
transaction: options
|
|
131
|
+
transaction: resolveTransaction(options),
|
|
133
132
|
format: "plain"
|
|
134
133
|
},
|
|
135
|
-
createJsonRestContext(options
|
|
134
|
+
createJsonRestContext(options.context)
|
|
136
135
|
)
|
|
137
136
|
);
|
|
138
137
|
}
|
|
139
138
|
|
|
140
139
|
async function lockWorkspaceForInvitations(workspaceId, options = {}) {
|
|
141
|
-
|
|
140
|
+
// readable-check: allow method-probe - platform object: a Knex transaction, which the workspace lock needs and options may omit
|
|
141
|
+
if (typeof options.trx !== "function" || options.trx.isCompleted()) {
|
|
142
142
|
throw new TypeError("Invitation locking requires an active transaction.");
|
|
143
143
|
}
|
|
144
144
|
const id = normalizeRecordId(workspaceId, { fallback: null });
|
|
@@ -227,7 +227,7 @@ function createRepository({ api } = {}) {
|
|
|
227
227
|
.sort((left, right) => String(right.createdAt || "").localeCompare(String(left.createdAt || "")));
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
-
async function insert(payload
|
|
230
|
+
async function insert(payload, options = {}) {
|
|
231
231
|
const source = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
232
232
|
const workspaceId = normalizeRecordId(source.workspaceId, { fallback: null });
|
|
233
233
|
if (!workspaceId) {
|
|
@@ -262,14 +262,14 @@ function createRepository({ api } = {}) {
|
|
|
262
262
|
},
|
|
263
263
|
format: "plain",
|
|
264
264
|
returning: "full",
|
|
265
|
-
transaction: options
|
|
265
|
+
transaction: resolveTransaction(options)
|
|
266
266
|
},
|
|
267
|
-
createJsonRestContext(options
|
|
267
|
+
createJsonRestContext(options.context)
|
|
268
268
|
);
|
|
269
269
|
|
|
270
270
|
return normalizeInviteRecord(created);
|
|
271
271
|
} catch (error) {
|
|
272
|
-
if (
|
|
272
|
+
if (!isLostDuplicateInsertRace(error, options)) {
|
|
273
273
|
throw error;
|
|
274
274
|
}
|
|
275
275
|
}
|
|
@@ -293,38 +293,38 @@ function createRepository({ api } = {}) {
|
|
|
293
293
|
return;
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const rows = await queryInvites(
|
|
302
|
-
{
|
|
303
|
-
workspace: normalizedWorkspaceId,
|
|
304
|
-
email: normalizeLowerText(email),
|
|
305
|
-
status: "pending"
|
|
306
|
-
},
|
|
307
|
-
{ ...options, lock: true }
|
|
308
|
-
);
|
|
309
|
-
|
|
310
|
-
for (const row of rows) {
|
|
311
|
-
if (!row?.id) {
|
|
312
|
-
continue;
|
|
313
|
-
}
|
|
314
|
-
await api.resources.workspaceInvites.patch(
|
|
296
|
+
await joinOrOpenTransaction(options, withTransaction, async (trx) => {
|
|
297
|
+
const transactionOptions = { ...options, trx };
|
|
298
|
+
await lockWorkspaceForInvitations(normalizedWorkspaceId, transactionOptions);
|
|
299
|
+
const patch = normalizeInvitePatchPayload({ status: "expired" });
|
|
300
|
+
const rows = await queryInvites(
|
|
315
301
|
{
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
updatedAt: new Date().toISOString()
|
|
320
|
-
},
|
|
321
|
-
format: "plain",
|
|
322
|
-
returning: "full",
|
|
323
|
-
transaction: options?.trx || null
|
|
302
|
+
workspace: normalizedWorkspaceId,
|
|
303
|
+
email: normalizeLowerText(email),
|
|
304
|
+
status: "pending"
|
|
324
305
|
},
|
|
325
|
-
|
|
306
|
+
{ ...transactionOptions, lock: true }
|
|
326
307
|
);
|
|
327
|
-
|
|
308
|
+
|
|
309
|
+
for (const row of rows) {
|
|
310
|
+
if (!row?.id) {
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
await api.resources.workspaceInvites.patch(
|
|
314
|
+
{
|
|
315
|
+
id: row.id,
|
|
316
|
+
data: {
|
|
317
|
+
status: patch.status,
|
|
318
|
+
updatedAt: new Date().toISOString()
|
|
319
|
+
},
|
|
320
|
+
format: "plain",
|
|
321
|
+
returning: "full",
|
|
322
|
+
transaction: trx
|
|
323
|
+
},
|
|
324
|
+
createJsonRestContext(options.context)
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
328
|
}
|
|
329
329
|
|
|
330
330
|
async function transitionPendingById(inviteId, status, options = {}) {
|
|
@@ -342,30 +342,29 @@ function createRepository({ api } = {}) {
|
|
|
342
342
|
}
|
|
343
343
|
workspaceId = normalizeInviteRecord(candidate[0]).workspaceId;
|
|
344
344
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
345
|
+
await joinOrOpenTransaction(options, withTransaction, async (trx) => {
|
|
346
|
+
const invite = await findPendingByIdForWorkspace(id, workspaceId, { ...options, trx, workspaceId, lock: true });
|
|
347
|
+
if (!invite) {
|
|
348
|
+
throw new AppError(404, "Invitation not found or already handled.", {
|
|
349
|
+
code: "workspaces.invite.invitationNotFoundOrHandled"
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
const updatedAt = new Date().toISOString();
|
|
353
|
+
await api.resources.workspaceInvites.patch(
|
|
354
|
+
{
|
|
355
|
+
id,
|
|
356
|
+
data: {
|
|
357
|
+
status,
|
|
358
|
+
[status === "accepted" ? "acceptedAt" : "revokedAt"]: updatedAt,
|
|
359
|
+
updatedAt
|
|
360
|
+
},
|
|
361
|
+
format: "plain",
|
|
362
|
+
returning: "full",
|
|
363
|
+
transaction: trx
|
|
362
364
|
},
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
},
|
|
367
|
-
createJsonRestContext(options.context || null)
|
|
368
|
-
);
|
|
365
|
+
createJsonRestContext(options.context)
|
|
366
|
+
);
|
|
367
|
+
});
|
|
369
368
|
}
|
|
370
369
|
|
|
371
370
|
async function markAcceptedById(inviteId, options = {}) {
|