@jskit-ai/workspaces-core 0.1.14 → 0.1.16
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 +2 -2
- package/package.json +18 -3
- package/src/server/WorkspacesCoreServiceProvider.js +41 -2
- package/src/server/common/contributors/workspaceActionContextContributor.js +88 -0
- package/src/server/common/contributors/workspaceAuthPolicyContextResolver.js +34 -0
- package/src/server/common/contributors/workspaceRouteVisibilityResolver.js +78 -0
- package/src/server/common/formatters/workspaceFormatter.js +53 -0
- package/src/server/common/repositories/repositoryUtils.js +59 -0
- package/src/server/common/repositories/workspaceInvitesRepository.js +208 -0
- package/src/server/common/repositories/workspaceMembershipsRepository.js +190 -0
- package/src/server/common/repositories/workspacesRepository.js +202 -0
- package/src/server/common/services/workspaceContextService.js +281 -0
- package/src/server/common/support/deepFreeze.js +1 -0
- package/src/server/common/support/realtimeServiceEvents.js +91 -0
- package/src/server/common/support/resolveActionUser.js +9 -0
- package/src/server/common/support/workspaceRoutePaths.js +18 -0
- package/src/server/common/validators/authenticatedUserValidator.js +43 -0
- package/src/server/common/validators/routeParamsValidator.js +62 -0
- package/src/server/registerWorkspaceBootstrap.js +27 -0
- package/src/server/registerWorkspaceCore.js +100 -0
- package/src/server/registerWorkspaceRepositories.js +26 -0
- package/src/server/support/resolveWorkspace.js +16 -0
- package/src/server/support/workspaceActionSurfaces.js +118 -0
- package/src/server/support/workspaceInvitationsPolicy.js +45 -0
- package/src/server/support/workspaceRouteInput.js +22 -0
- package/src/server/workspaceBootstrapContributor.js +233 -0
- package/src/server/workspaceDirectory/bootWorkspaceDirectoryRoutes.js +133 -0
- package/src/server/workspaceDirectory/registerWorkspaceDirectory.js +19 -0
- package/src/server/workspaceDirectory/workspaceDirectoryActions.js +133 -0
- package/src/server/workspaceMembers/bootWorkspaceMembers.js +236 -0
- package/src/server/workspaceMembers/registerWorkspaceMembers.js +108 -0
- package/src/server/workspaceMembers/workspaceMembersActions.js +186 -0
- package/src/server/workspaceMembers/workspaceMembersService.js +222 -0
- package/src/server/workspacePendingInvitations/bootWorkspacePendingInvitations.js +62 -0
- package/src/server/workspacePendingInvitations/registerWorkspacePendingInvitations.js +119 -0
- package/src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js +74 -0
- package/src/server/workspacePendingInvitations/workspacePendingInvitationsService.js +138 -0
- package/src/server/workspaceSettings/bootWorkspaceSettings.js +76 -0
- package/src/server/workspaceSettings/registerWorkspaceSettings.js +62 -0
- package/src/server/workspaceSettings/workspaceSettingsActions.js +72 -0
- package/src/server/workspaceSettings/workspaceSettingsRepository.js +154 -0
- package/src/server/workspaceSettings/workspaceSettingsService.js +66 -0
- package/src/shared/operationMessages.js +16 -0
- package/src/shared/resources/resolveGlobalArrayRegistry.js +6 -0
- package/src/shared/resources/workspaceMembersResource.js +354 -0
- package/src/shared/resources/workspacePendingInvitationsResource.js +82 -0
- package/src/shared/resources/workspaceResource.js +176 -0
- package/src/shared/resources/workspaceSettingsFields.js +59 -0
- package/src/shared/resources/workspaceSettingsResource.js +169 -0
- package/src/shared/roles.js +161 -0
- package/src/shared/settings.js +119 -0
- package/src/shared/support/workspacePathModel.js +145 -0
- package/src/shared/tenancyMode.js +35 -0
- package/src/shared/tenancyProfile.js +73 -0
- package/templates/packages/main/src/shared/resources/workspaceSettingsFields.js +2 -2
- package/test/registerServiceRealtimeEvents.test.js +116 -0
- package/test/registerWorkspaceDirectory.test.js +31 -0
- package/test/registerWorkspaceSettings.test.js +40 -0
- package/test/repositoryContracts.test.js +34 -0
- package/test/resourcesCanonical.test.js +74 -0
- package/test/roles.test.js +159 -0
- package/test/routeParamsValidator.test.js +49 -0
- package/test/settingsFieldRegistriesSingleton.test.js +14 -0
- package/test/tenancyProfile.test.js +67 -0
- package/test/usersRouteResources.test.js +97 -0
- package/test/workspaceActionContextContributor.test.js +344 -0
- package/test/workspaceActionSurfaces.test.js +85 -0
- package/test/workspaceAuthPolicyContextResolver.test.js +119 -0
- package/test/workspaceBootstrapContributor.test.js +169 -0
- package/test/workspaceInvitationsPolicy.test.js +71 -0
- package/test/workspaceInvitesRepository.test.js +111 -0
- package/test/workspaceMembersService.test.js +398 -0
- package/test/workspacePathModel.test.js +93 -0
- package/test/workspacePendingInvitationsResource.test.js +38 -0
- package/test/workspacePendingInvitationsService.test.js +151 -0
- package/test/workspaceRouteVisibilityResolver.test.js +83 -0
- package/test/workspaceService.test.js +546 -0
- package/test/workspaceSettingsActions.test.js +52 -0
- package/test/workspaceSettingsRepository.test.js +202 -0
- package/test/workspaceSettingsResource.test.js +169 -0
- package/test/workspaceSettingsService.test.js +140 -0
- package/test/workspacesRouteRequestInputValidator.test.js +5 -5
- package/test-support/registerDefaultSettingsFields.js +1 -0
package/package.descriptor.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export default Object.freeze({
|
|
2
2
|
packageVersion: 1,
|
|
3
3
|
packageId: "@jskit-ai/workspaces-core",
|
|
4
|
-
version: "0.1.
|
|
4
|
+
version: "0.1.16",
|
|
5
5
|
kind: "runtime",
|
|
6
6
|
description: "Workspace tenancy runtime plus HTTP routes, role catalog, and workspace config scaffolding.",
|
|
7
7
|
dependsOn: [
|
|
@@ -110,7 +110,7 @@ export default Object.freeze({
|
|
|
110
110
|
mutations: {
|
|
111
111
|
dependencies: {
|
|
112
112
|
runtime: {
|
|
113
|
-
"@jskit-ai/users-core": "0.1.
|
|
113
|
+
"@jskit-ai/users-core": "0.1.50"
|
|
114
114
|
},
|
|
115
115
|
dev: {}
|
|
116
116
|
},
|
package/package.json
CHANGED
|
@@ -1,14 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/workspaces-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
7
7
|
},
|
|
8
8
|
"exports": {
|
|
9
|
-
"./server/WorkspacesCoreServiceProvider": "./src/server/WorkspacesCoreServiceProvider.js"
|
|
9
|
+
"./server/WorkspacesCoreServiceProvider": "./src/server/WorkspacesCoreServiceProvider.js",
|
|
10
|
+
"./server/validators/routeParamsValidator": "./src/server/common/validators/routeParamsValidator.js",
|
|
11
|
+
"./server/support/resolveWorkspace": "./src/server/support/resolveWorkspace.js",
|
|
12
|
+
"./server/support/workspaceRouteInput": "./src/server/support/workspaceRouteInput.js",
|
|
13
|
+
"./shared/settings": "./src/shared/settings.js",
|
|
14
|
+
"./shared/tenancyProfile": "./src/shared/tenancyProfile.js",
|
|
15
|
+
"./shared/support/workspacePathModel": "./src/shared/support/workspacePathModel.js",
|
|
16
|
+
"./shared/resources/workspaceResource": "./src/shared/resources/workspaceResource.js",
|
|
17
|
+
"./shared/resources/workspaceSettingsFields": "./src/shared/resources/workspaceSettingsFields.js",
|
|
18
|
+
"./shared/resources/workspaceSettingsResource": "./src/shared/resources/workspaceSettingsResource.js"
|
|
10
19
|
},
|
|
11
20
|
"dependencies": {
|
|
12
|
-
"@
|
|
21
|
+
"@fastify/type-provider-typebox": "^6.1.0",
|
|
22
|
+
"@jskit-ai/auth-core": "0.1.39",
|
|
23
|
+
"@jskit-ai/database-runtime": "0.1.40",
|
|
24
|
+
"@jskit-ai/http-runtime": "0.1.39",
|
|
25
|
+
"@jskit-ai/kernel": "0.1.40",
|
|
26
|
+
"@jskit-ai/users-core": "0.1.50",
|
|
27
|
+
"typebox": "^1.0.81"
|
|
13
28
|
}
|
|
14
29
|
}
|
|
@@ -1,5 +1,44 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { bootWorkspaceDirectoryRoutes } from "./workspaceDirectory/bootWorkspaceDirectoryRoutes.js";
|
|
2
|
+
import { registerWorkspaceDirectory } from "./workspaceDirectory/registerWorkspaceDirectory.js";
|
|
3
|
+
import {
|
|
4
|
+
registerWorkspacePendingInvitations
|
|
5
|
+
} from "./workspacePendingInvitations/registerWorkspacePendingInvitations.js";
|
|
6
|
+
import { bootWorkspacePendingInvitations } from "./workspacePendingInvitations/bootWorkspacePendingInvitations.js";
|
|
7
|
+
import { registerWorkspaceMembers } from "./workspaceMembers/registerWorkspaceMembers.js";
|
|
8
|
+
import { bootWorkspaceMembers } from "./workspaceMembers/bootWorkspaceMembers.js";
|
|
9
|
+
import { registerWorkspaceSettings } from "./workspaceSettings/registerWorkspaceSettings.js";
|
|
10
|
+
import { bootWorkspaceSettings } from "./workspaceSettings/bootWorkspaceSettings.js";
|
|
11
|
+
import { registerWorkspaceRepositories } from "./registerWorkspaceRepositories.js";
|
|
12
|
+
import { registerWorkspaceCore } from "./registerWorkspaceCore.js";
|
|
13
|
+
import { registerWorkspaceBootstrap } from "./registerWorkspaceBootstrap.js";
|
|
2
14
|
|
|
3
|
-
|
|
15
|
+
class WorkspacesCoreServiceProvider {
|
|
16
|
+
static id = "workspaces.core";
|
|
17
|
+
|
|
18
|
+
static dependsOn = ["users.core"];
|
|
19
|
+
|
|
20
|
+
register(app) {
|
|
21
|
+
registerWorkspaceRepositories(app);
|
|
22
|
+
registerWorkspaceCore(app);
|
|
23
|
+
registerWorkspaceBootstrap(app);
|
|
24
|
+
registerWorkspaceDirectory(app);
|
|
25
|
+
registerWorkspaceMembers(app);
|
|
26
|
+
registerWorkspaceSettings(app);
|
|
27
|
+
registerWorkspacePendingInvitations(app);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async boot(app) {
|
|
31
|
+
if (app.make("workspaces.enabled") !== true) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
bootWorkspaceDirectoryRoutes(app);
|
|
36
|
+
if (app.make("workspaces.invitations.enabled") === true) {
|
|
37
|
+
bootWorkspacePendingInvitations(app);
|
|
38
|
+
}
|
|
39
|
+
bootWorkspaceSettings(app);
|
|
40
|
+
bootWorkspaceMembers(app);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
4
43
|
|
|
5
44
|
export { WorkspacesCoreServiceProvider };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import {
|
|
2
|
+
normalizeObject,
|
|
3
|
+
requireServiceMethod
|
|
4
|
+
} from "@jskit-ai/kernel/shared/actions/actionContributorHelpers";
|
|
5
|
+
import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
|
|
6
|
+
import {
|
|
7
|
+
checkRouteVisibility,
|
|
8
|
+
ROUTE_VISIBILITY_PUBLIC,
|
|
9
|
+
ROUTE_VISIBILITY_WORKSPACE,
|
|
10
|
+
ROUTE_VISIBILITY_WORKSPACE_USER
|
|
11
|
+
} from "@jskit-ai/kernel/shared/support/visibility";
|
|
12
|
+
import { resolveActionUser } from "../support/resolveActionUser.js";
|
|
13
|
+
const WORKSPACE_VISIBILITY_ACTION_CONTEXT_SET = new Set([
|
|
14
|
+
ROUTE_VISIBILITY_WORKSPACE,
|
|
15
|
+
ROUTE_VISIBILITY_WORKSPACE_USER
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
function normalizeWorkspaceSurfaceIds(surfaceIds = []) {
|
|
19
|
+
const source = Array.isArray(surfaceIds) ? surfaceIds : [];
|
|
20
|
+
const normalized = new Set();
|
|
21
|
+
|
|
22
|
+
for (const entry of source) {
|
|
23
|
+
const surfaceId = normalizeSurfaceId(entry);
|
|
24
|
+
if (!surfaceId) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
normalized.add(surfaceId);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return normalized;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function createWorkspaceActionContextContributor({ workspaceService, workspaceSurfaceIds = [] } = {}) {
|
|
34
|
+
const contributorId = "workspaces.context";
|
|
35
|
+
const workspaceSurfaceIdSet = normalizeWorkspaceSurfaceIds(workspaceSurfaceIds);
|
|
36
|
+
|
|
37
|
+
requireServiceMethod(workspaceService, "resolveWorkspaceContextForUserBySlug", contributorId);
|
|
38
|
+
|
|
39
|
+
return Object.freeze({
|
|
40
|
+
contributorId,
|
|
41
|
+
async contribute({ definition = null, input, context, request } = {}) {
|
|
42
|
+
const payload = normalizeObject(input);
|
|
43
|
+
if (!Object.hasOwn(payload, "workspaceSlug")) {
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const actionSurfaces = Array.isArray(definition?.surfaces) ? definition.surfaces : [];
|
|
48
|
+
const hasWorkspaceActionSurface = actionSurfaces.some((surfaceId) => workspaceSurfaceIdSet.has(surfaceId));
|
|
49
|
+
const routeSurfaceId = normalizeSurfaceId(request?.routeOptions?.config?.surface);
|
|
50
|
+
const hasWorkspaceSurface = workspaceSurfaceIdSet.has(routeSurfaceId);
|
|
51
|
+
const routeVisibilityInput =
|
|
52
|
+
request && request.routeOptions && request.routeOptions.config
|
|
53
|
+
? request.routeOptions.config.visibility
|
|
54
|
+
: ROUTE_VISIBILITY_PUBLIC;
|
|
55
|
+
const routeVisibility = checkRouteVisibility(routeVisibilityInput);
|
|
56
|
+
const hasWorkspaceRouteVisibility = WORKSPACE_VISIBILITY_ACTION_CONTEXT_SET.has(routeVisibility);
|
|
57
|
+
if (!hasWorkspaceActionSurface && !hasWorkspaceRouteVisibility && !hasWorkspaceSurface) {
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const resolvedWorkspaceContext = await workspaceService.resolveWorkspaceContextForUserBySlug(
|
|
62
|
+
resolveActionUser(context, payload),
|
|
63
|
+
payload.workspaceSlug,
|
|
64
|
+
{ request }
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const contribution = {
|
|
68
|
+
requestMeta: {
|
|
69
|
+
resolvedWorkspaceContext
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
if (!context?.workspace) {
|
|
74
|
+
contribution.workspace = resolvedWorkspaceContext.workspace;
|
|
75
|
+
}
|
|
76
|
+
if (!context?.membership) {
|
|
77
|
+
contribution.membership = resolvedWorkspaceContext.membership;
|
|
78
|
+
}
|
|
79
|
+
if (!Array.isArray(context?.permissions) || context.permissions.length < 1) {
|
|
80
|
+
contribution.permissions = resolvedWorkspaceContext.permissions;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return contribution;
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export { createWorkspaceActionContextContributor };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
+
|
|
3
|
+
function createWorkspaceAuthPolicyContextResolver({ workspaceService } = {}) {
|
|
4
|
+
if (!workspaceService || typeof workspaceService.resolveWorkspaceContextForUserBySlug !== "function") {
|
|
5
|
+
throw new Error(
|
|
6
|
+
"workspace auth policy context resolver requires workspaceService.resolveWorkspaceContextForUserBySlug()."
|
|
7
|
+
);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return async function resolveWorkspaceAuthPolicyContext({ request, actor, meta } = {}) {
|
|
11
|
+
const contextPolicy = normalizeText(meta?.contextPolicy || "none").toLowerCase() || "none";
|
|
12
|
+
const permission = normalizeText(meta?.permission);
|
|
13
|
+
if (contextPolicy === "none" && !permission) {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const workspaceSlug = normalizeText(request?.params?.workspaceSlug).toLowerCase();
|
|
18
|
+
if (!workspaceSlug || !actor) {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const resolvedWorkspaceContext = await workspaceService.resolveWorkspaceContextForUserBySlug(actor, workspaceSlug, {
|
|
23
|
+
request
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
workspace: resolvedWorkspaceContext?.workspace || null,
|
|
28
|
+
membership: resolvedWorkspaceContext?.membership || null,
|
|
29
|
+
permissions: Array.isArray(resolvedWorkspaceContext?.permissions) ? resolvedWorkspaceContext.permissions : []
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export { createWorkspaceAuthPolicyContextResolver };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { normalizeOpaqueId, normalizeRecordId, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
+
|
|
3
|
+
function buildVisibilityContribution({ visibility, scopeOwnerId = null, userId = null } = {}) {
|
|
4
|
+
const requiresActorScope = visibility === "workspace_user";
|
|
5
|
+
const contribution = {
|
|
6
|
+
scopeKind: requiresActorScope ? "workspace_user" : "workspace",
|
|
7
|
+
requiresActorScope
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
if (scopeOwnerId) {
|
|
11
|
+
contribution.scopeOwnerId = scopeOwnerId;
|
|
12
|
+
}
|
|
13
|
+
if (requiresActorScope && userId != null) {
|
|
14
|
+
contribution.userId = userId;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return contribution;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function createWorkspaceRouteVisibilityResolver({ workspaceService } = {}) {
|
|
21
|
+
if (!workspaceService || typeof workspaceService.resolveWorkspaceContextForUserBySlug !== "function") {
|
|
22
|
+
throw new Error("workspace route visibility resolver requires workspaceService.resolveWorkspaceContextForUserBySlug().");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return Object.freeze({
|
|
26
|
+
resolverId: "workspaces.visibility",
|
|
27
|
+
async resolve({ visibility, context, request, input } = {}) {
|
|
28
|
+
if (visibility !== "workspace" && visibility !== "workspace_user") {
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const actor = context?.actor || request?.user || null;
|
|
33
|
+
const userId = normalizeOpaqueId(actor?.id);
|
|
34
|
+
const workspace =
|
|
35
|
+
context?.workspace || context?.requestMeta?.resolvedWorkspaceContext?.workspace || request?.workspace || null;
|
|
36
|
+
const scopeOwnerId = normalizeRecordId(workspace?.id, { fallback: null });
|
|
37
|
+
if (!scopeOwnerId) {
|
|
38
|
+
const workspaceSlug = normalizeText(input?.workspaceSlug).toLowerCase();
|
|
39
|
+
|
|
40
|
+
if (!workspaceSlug || !actor) {
|
|
41
|
+
return visibility === "workspace_user"
|
|
42
|
+
? buildVisibilityContribution({
|
|
43
|
+
visibility,
|
|
44
|
+
userId
|
|
45
|
+
})
|
|
46
|
+
: {};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const resolvedWorkspaceContext = await workspaceService.resolveWorkspaceContextForUserBySlug(actor, workspaceSlug, {
|
|
50
|
+
request
|
|
51
|
+
});
|
|
52
|
+
const resolvedWorkspaceOwnerId = normalizeRecordId(resolvedWorkspaceContext?.workspace?.id, { fallback: null });
|
|
53
|
+
if (!resolvedWorkspaceOwnerId) {
|
|
54
|
+
return visibility === "workspace_user"
|
|
55
|
+
? buildVisibilityContribution({
|
|
56
|
+
visibility,
|
|
57
|
+
userId
|
|
58
|
+
})
|
|
59
|
+
: {};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return buildVisibilityContribution({
|
|
63
|
+
visibility,
|
|
64
|
+
scopeOwnerId: resolvedWorkspaceOwnerId,
|
|
65
|
+
userId
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return buildVisibilityContribution({
|
|
70
|
+
visibility,
|
|
71
|
+
scopeOwnerId,
|
|
72
|
+
userId
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export { createWorkspaceRouteVisibilityResolver };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { resolveWorkspaceThemePalettes } from "@jskit-ai/workspaces-core/shared/settings";
|
|
2
|
+
import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/actions/textNormalization";
|
|
3
|
+
import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
|
|
4
|
+
|
|
5
|
+
function mapWorkspaceSummary(workspace, membership) {
|
|
6
|
+
return {
|
|
7
|
+
id: normalizeRecordId(workspace.id, { fallback: "" }),
|
|
8
|
+
slug: normalizeText(workspace.slug),
|
|
9
|
+
name: normalizeText(workspace.name),
|
|
10
|
+
avatarUrl: normalizeText(workspace.avatarUrl),
|
|
11
|
+
roleSid: normalizeLowerText(membership?.roleSid || "member") || "member",
|
|
12
|
+
isAccessible: normalizeLowerText(membership?.status || "active") === "active"
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function mapWorkspaceSettingsPublic(workspaceSettings, { workspaceInvitationsEnabled = true } = {}) {
|
|
17
|
+
const source = workspaceSettings && typeof workspaceSettings === "object" ? workspaceSettings : {};
|
|
18
|
+
const invitesAvailable = workspaceInvitationsEnabled === true;
|
|
19
|
+
const invitesEnabled = invitesAvailable && source.invitesEnabled !== false;
|
|
20
|
+
const themePalettes = resolveWorkspaceThemePalettes(source);
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
lightPrimaryColor: themePalettes.light.color,
|
|
24
|
+
lightSecondaryColor: themePalettes.light.secondaryColor,
|
|
25
|
+
lightSurfaceColor: themePalettes.light.surfaceColor,
|
|
26
|
+
lightSurfaceVariantColor: themePalettes.light.surfaceVariantColor,
|
|
27
|
+
darkPrimaryColor: themePalettes.dark.color,
|
|
28
|
+
darkSecondaryColor: themePalettes.dark.secondaryColor,
|
|
29
|
+
darkSurfaceColor: themePalettes.dark.surfaceColor,
|
|
30
|
+
darkSurfaceVariantColor: themePalettes.dark.surfaceVariantColor,
|
|
31
|
+
invitesEnabled,
|
|
32
|
+
invitesAvailable,
|
|
33
|
+
invitesEffective: invitesAvailable && invitesEnabled
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function mapMembershipSummary(membership, workspace) {
|
|
38
|
+
if (!membership) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
workspaceId: normalizeRecordId(workspace?.id || membership.workspaceId, { fallback: "" }),
|
|
44
|
+
roleSid: normalizeLowerText(membership.roleSid || "member") || "member",
|
|
45
|
+
status: normalizeLowerText(membership.status || "active") || "active"
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export {
|
|
50
|
+
mapMembershipSummary,
|
|
51
|
+
mapWorkspaceSettingsPublic,
|
|
52
|
+
mapWorkspaceSummary
|
|
53
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {
|
|
2
|
+
normalizeDbRecordId,
|
|
3
|
+
toInsertDateTime,
|
|
4
|
+
toNullableDateTime,
|
|
5
|
+
toIsoString,
|
|
6
|
+
createWithTransaction
|
|
7
|
+
} from "@jskit-ai/database-runtime/shared";
|
|
8
|
+
import { isDuplicateEntryError } from "@jskit-ai/database-runtime/shared/duplicateEntry";
|
|
9
|
+
import { normalizeLowerText, normalizeRecordId, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
10
|
+
|
|
11
|
+
function nowDb() {
|
|
12
|
+
return toInsertDateTime();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function toNullableIso(value) {
|
|
16
|
+
if (!value) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
return toIsoString(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function uniqueSorted(values) {
|
|
23
|
+
return [...new Set(values)].sort((left, right) => String(left).localeCompare(String(right)));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseJson(value, fallback = {}) {
|
|
27
|
+
if (value == null) {
|
|
28
|
+
return fallback;
|
|
29
|
+
}
|
|
30
|
+
if (typeof value === "object") {
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(String(value));
|
|
35
|
+
} catch {
|
|
36
|
+
return fallback;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function toDbJson(value, fallback = {}) {
|
|
41
|
+
const source = value && typeof value === "object" ? value : fallback;
|
|
42
|
+
return JSON.stringify(source);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export {
|
|
46
|
+
toNullableDateTime,
|
|
47
|
+
toIsoString,
|
|
48
|
+
isDuplicateEntryError,
|
|
49
|
+
normalizeText,
|
|
50
|
+
normalizeLowerText,
|
|
51
|
+
normalizeRecordId,
|
|
52
|
+
normalizeDbRecordId,
|
|
53
|
+
nowDb,
|
|
54
|
+
toNullableIso,
|
|
55
|
+
uniqueSorted,
|
|
56
|
+
parseJson,
|
|
57
|
+
toDbJson,
|
|
58
|
+
createWithTransaction
|
|
59
|
+
};
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { resolveInsertedRecordId } from "@jskit-ai/database-runtime/shared";
|
|
2
|
+
import {
|
|
3
|
+
normalizeLowerText,
|
|
4
|
+
normalizeDbRecordId,
|
|
5
|
+
normalizeRecordId,
|
|
6
|
+
normalizeText,
|
|
7
|
+
toIsoString,
|
|
8
|
+
toNullableIso,
|
|
9
|
+
toNullableDateTime,
|
|
10
|
+
nowDb,
|
|
11
|
+
isDuplicateEntryError,
|
|
12
|
+
createWithTransaction
|
|
13
|
+
} from "./repositoryUtils.js";
|
|
14
|
+
|
|
15
|
+
function mapRow(row) {
|
|
16
|
+
if (!row) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
id: normalizeDbRecordId(row.id, { fallback: "" }),
|
|
22
|
+
workspaceId: normalizeDbRecordId(row.workspace_id, { fallback: "" }),
|
|
23
|
+
email: normalizeLowerText(row.email),
|
|
24
|
+
roleSid: normalizeLowerText(row.role_sid || "member") || "member",
|
|
25
|
+
status: normalizeLowerText(row.status || "pending") || "pending",
|
|
26
|
+
tokenHash: normalizeText(row.token_hash),
|
|
27
|
+
invitedByUserId: row.invited_by_user_id == null ? null : normalizeDbRecordId(row.invited_by_user_id, { fallback: null }),
|
|
28
|
+
expiresAt: toNullableIso(row.expires_at),
|
|
29
|
+
acceptedAt: toNullableIso(row.accepted_at),
|
|
30
|
+
revokedAt: toNullableIso(row.revoked_at),
|
|
31
|
+
createdAt: toIsoString(row.created_at),
|
|
32
|
+
updatedAt: toIsoString(row.updated_at),
|
|
33
|
+
workspaceSlug: row.workspace_slug ? normalizeText(row.workspace_slug) : undefined,
|
|
34
|
+
workspaceName: row.workspace_name ? normalizeText(row.workspace_name) : undefined,
|
|
35
|
+
workspaceAvatarUrl: row.workspace_avatar_url ? normalizeText(row.workspace_avatar_url) : undefined
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const WORKSPACE_INVITE_WITH_WORKSPACE_SELECT = Object.freeze([
|
|
40
|
+
"wi.*",
|
|
41
|
+
"w.slug as workspace_slug",
|
|
42
|
+
"w.name as workspace_name",
|
|
43
|
+
"w.avatar_url as workspace_avatar_url"
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
function createRepository(knex) {
|
|
47
|
+
if (typeof knex !== "function") {
|
|
48
|
+
throw new TypeError("workspaceInvitesRepository requires knex.");
|
|
49
|
+
}
|
|
50
|
+
const withTransaction = createWithTransaction(knex);
|
|
51
|
+
|
|
52
|
+
async function findPendingByTokenHash(tokenHash, options = {}) {
|
|
53
|
+
const client = options?.trx || knex;
|
|
54
|
+
const row = await client("workspace_invites")
|
|
55
|
+
.where({ token_hash: normalizeText(tokenHash), status: "pending" })
|
|
56
|
+
.first();
|
|
57
|
+
return mapRow(row);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function listPendingByEmail(email, options = {}) {
|
|
61
|
+
const client = options?.trx || knex;
|
|
62
|
+
const normalizedEmail = normalizeLowerText(email);
|
|
63
|
+
if (!normalizedEmail) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const rows = await client("workspace_invites as wi")
|
|
68
|
+
.join("workspaces as w", "w.id", "wi.workspace_id")
|
|
69
|
+
.where({ "wi.email": normalizedEmail, "wi.status": "pending" })
|
|
70
|
+
.orderBy("wi.created_at", "desc")
|
|
71
|
+
.select(WORKSPACE_INVITE_WITH_WORKSPACE_SELECT);
|
|
72
|
+
|
|
73
|
+
return rows.map(mapRow).filter(Boolean);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function listPendingByWorkspaceIdWithWorkspace(workspaceId, options = {}) {
|
|
77
|
+
const normalizedWorkspaceId = normalizeRecordId(workspaceId, { fallback: null });
|
|
78
|
+
if (!normalizedWorkspaceId) {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const client = options?.trx || knex;
|
|
83
|
+
const rows = await client("workspace_invites as wi")
|
|
84
|
+
.join("workspaces as w", "w.id", "wi.workspace_id")
|
|
85
|
+
.where({ "wi.workspace_id": normalizedWorkspaceId, "wi.status": "pending" })
|
|
86
|
+
.orderBy("wi.created_at", "desc")
|
|
87
|
+
.select(WORKSPACE_INVITE_WITH_WORKSPACE_SELECT);
|
|
88
|
+
|
|
89
|
+
return rows.map(mapRow).filter(Boolean);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function insert(payload = {}, options = {}) {
|
|
93
|
+
const client = options?.trx || knex;
|
|
94
|
+
const source = payload && typeof payload === "object" ? payload : {};
|
|
95
|
+
const workspaceId = normalizeRecordId(source.workspaceId, { fallback: null });
|
|
96
|
+
if (!workspaceId) {
|
|
97
|
+
throw new TypeError("workspaceInvitesRepository.insert requires workspaceId.");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const insertPayload = {
|
|
101
|
+
workspace_id: workspaceId,
|
|
102
|
+
email: normalizeLowerText(source.email),
|
|
103
|
+
role_sid: normalizeLowerText(source.roleSid || "member") || "member",
|
|
104
|
+
status: normalizeLowerText(source.status || "pending") || "pending",
|
|
105
|
+
token_hash: normalizeText(source.tokenHash),
|
|
106
|
+
invited_by_user_id: source.invitedByUserId == null ? null : normalizeRecordId(source.invitedByUserId, { fallback: null }),
|
|
107
|
+
expires_at: toNullableDateTime(source.expiresAt),
|
|
108
|
+
accepted_at: null,
|
|
109
|
+
revoked_at: null,
|
|
110
|
+
created_at: nowDb(),
|
|
111
|
+
updated_at: nowDb()
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const result = await client("workspace_invites").insert(insertPayload);
|
|
116
|
+
const insertedId = resolveInsertedRecordId(result, { fallback: null });
|
|
117
|
+
if (insertedId) {
|
|
118
|
+
const row = await client("workspace_invites").where({ id: insertedId }).first();
|
|
119
|
+
return mapRow(row);
|
|
120
|
+
}
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (!isDuplicateEntryError(error)) {
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const row = await client("workspace_invites")
|
|
128
|
+
.where({ workspace_id: insertPayload.workspace_id, email: insertPayload.email, status: "pending" })
|
|
129
|
+
.orderBy("id", "desc")
|
|
130
|
+
.first();
|
|
131
|
+
return mapRow(row);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function expirePendingByWorkspaceIdAndEmail(workspaceId, email, options = {}) {
|
|
135
|
+
const normalizedWorkspaceId = normalizeRecordId(workspaceId, { fallback: null });
|
|
136
|
+
if (!normalizedWorkspaceId) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const client = options?.trx || knex;
|
|
141
|
+
await client("workspace_invites")
|
|
142
|
+
.where({ workspace_id: normalizedWorkspaceId, email: normalizeLowerText(email), status: "pending" })
|
|
143
|
+
.update({
|
|
144
|
+
status: "expired",
|
|
145
|
+
updated_at: nowDb()
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function markAcceptedById(inviteId, options = {}) {
|
|
150
|
+
const normalizedInviteId = normalizeRecordId(inviteId, { fallback: null });
|
|
151
|
+
if (!normalizedInviteId) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const client = options?.trx || knex;
|
|
156
|
+
await client("workspace_invites")
|
|
157
|
+
.where({ id: normalizedInviteId })
|
|
158
|
+
.update({
|
|
159
|
+
status: "accepted",
|
|
160
|
+
accepted_at: nowDb(),
|
|
161
|
+
updated_at: nowDb()
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function revokeById(inviteId, options = {}) {
|
|
166
|
+
const normalizedInviteId = normalizeRecordId(inviteId, { fallback: null });
|
|
167
|
+
if (!normalizedInviteId) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const client = options?.trx || knex;
|
|
172
|
+
await client("workspace_invites")
|
|
173
|
+
.where({ id: normalizedInviteId })
|
|
174
|
+
.update({
|
|
175
|
+
status: "revoked",
|
|
176
|
+
revoked_at: nowDb(),
|
|
177
|
+
updated_at: nowDb()
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function findPendingByIdForWorkspace(inviteId, workspaceId, options = {}) {
|
|
182
|
+
const normalizedInviteId = normalizeRecordId(inviteId, { fallback: null });
|
|
183
|
+
const normalizedWorkspaceId = normalizeRecordId(workspaceId, { fallback: null });
|
|
184
|
+
if (!normalizedInviteId || !normalizedWorkspaceId) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const client = options?.trx || knex;
|
|
189
|
+
const row = await client("workspace_invites")
|
|
190
|
+
.where({ id: normalizedInviteId, workspace_id: normalizedWorkspaceId, status: "pending" })
|
|
191
|
+
.first();
|
|
192
|
+
return mapRow(row);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return Object.freeze({
|
|
196
|
+
withTransaction,
|
|
197
|
+
findPendingByTokenHash,
|
|
198
|
+
listPendingByEmail,
|
|
199
|
+
listPendingByWorkspaceIdWithWorkspace,
|
|
200
|
+
insert,
|
|
201
|
+
expirePendingByWorkspaceIdAndEmail,
|
|
202
|
+
markAcceptedById,
|
|
203
|
+
revokeById,
|
|
204
|
+
findPendingByIdForWorkspace
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export { createRepository, mapRow };
|