@jskit-ai/workspaces-core 0.2.4 → 0.2.6

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.
Files changed (75) hide show
  1. package/locales/en.json +2 -2
  2. package/package.json +38 -11
  3. package/patterns/workspace-server/PATTERN.md +42 -17
  4. package/patterns/workspace-server/example/packages/main/src/server/WorkspaceInvitationsFeature.js +27 -0
  5. package/patterns/workspace-server/example/packages/main/src/server/email/workspaceInviteEmail.js +11 -10
  6. package/patterns/workspace-server/example/packages/main/src/server/email/workspaceInviteMailer.js +27 -0
  7. package/src/server/WorkspacesFeature.js +74 -183
  8. package/src/server/WorkspacesIntegrationsProvider.js +26 -10
  9. package/src/server/common/contributors/workspaceActionContextContributor.js +60 -70
  10. package/src/server/common/contributors/workspaceAuthPolicyContextResolver.js +12 -27
  11. package/src/server/common/contributors/workspaceRouteVisibilityResolver.js +48 -63
  12. package/src/server/common/formatters/workspaceFormatter.js +12 -10
  13. package/src/server/common/repositories/workspaceInvitesRepository.js +74 -75
  14. package/src/server/common/repositories/workspaceMembershipsRepository.js +23 -45
  15. package/src/server/common/repositories/workspacesRepository.js +17 -25
  16. package/src/server/common/services/{workspaceContextService.js → workspaceDirectoryService.js} +52 -72
  17. package/src/server/common/support/realtimeServiceEvents.js +20 -12
  18. package/src/server/common/support/workspaceRecordId.js +21 -0
  19. package/src/server/common/validators/routeParamsValidator.js +3 -2
  20. package/src/server/i18n.js +5 -2
  21. package/src/server/support/resolveWorkspace.js +10 -7
  22. package/src/server/support/workspaceActionSurfaces.js +5 -41
  23. package/src/server/support/workspaceCrud.js +25 -0
  24. package/src/server/support/workspaceInvitationsPolicy.js +16 -27
  25. package/src/server/support/workspaceRouteInput.js +19 -2
  26. package/src/server/{common/support → support}/workspaceRoutePaths.js +8 -2
  27. package/src/server/types.js +47 -0
  28. package/src/server/workspaceBootstrapContributor.js +105 -142
  29. package/src/server/workspaceDirectory/workspaceDirectoryActions.js +105 -137
  30. package/src/server/workspaceDirectory/{bootWorkspaceDirectoryRoutes.js → workspaceDirectoryRoutes.js} +7 -19
  31. package/src/server/workspaceMembers/defaultWorkspaceInviteEmail.js +15 -29
  32. package/src/server/workspaceMembers/unconfiguredWorkspaceInviteMailer.js +25 -0
  33. package/src/server/workspaceMembers/workspaceInviteUrls.js +24 -23
  34. package/src/server/workspaceMembers/workspaceMembersActions.js +189 -219
  35. package/src/server/workspaceMembers/{bootWorkspaceMembers.js → workspaceMembersRoutes.js} +14 -26
  36. package/src/server/workspaceMembers/workspaceMembersService.js +110 -97
  37. package/src/server/workspacePendingInvitations/noInviteAcceptanceParticipant.js +8 -0
  38. package/src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js +79 -112
  39. package/src/server/workspacePendingInvitations/{bootWorkspacePendingInvitations.js → workspacePendingInvitationsRoutes.js} +11 -8
  40. package/src/server/workspacePendingInvitations/workspacePendingInvitationsService.js +54 -57
  41. package/src/server/workspaceSettings/workspaceSettingsActions.js +53 -73
  42. package/src/server/workspaceSettings/workspaceSettingsRepository.js +20 -20
  43. package/src/server/workspaceSettings/{bootWorkspaceSettings.js → workspaceSettingsRoutes.js} +9 -21
  44. package/src/server/workspaceSettings/workspaceSettingsService.js +24 -26
  45. package/src/server/workspacesRuntime.js +185 -0
  46. package/src/shared/operationMessages.js +6 -15
  47. package/src/shared/resources/workspaceMembersResource.js +2 -2
  48. package/src/shared/resources/workspacePendingInvitationsResource.js +2 -2
  49. package/src/shared/resources/workspaceResource.js +4 -1
  50. package/src/shared/roles.js +15 -12
  51. package/src/shared/tenancyProfile.js +2 -2
  52. package/test/exportsContract.test.js +5 -0
  53. package/test/featureRuntime.test.js +5 -6
  54. package/test/packageMetadata.test.js +12 -0
  55. package/test/repositories.integration.test.js +1 -1
  56. package/test/requestedWorkspaceScope.test.js +24 -0
  57. package/test/roleLabels.test.js +29 -7
  58. package/test/support/invitationTransactions.js +35 -19
  59. package/test/workspaceActionSurfaces.test.js +0 -55
  60. package/test/workspaceAuthPolicyContextResolver.test.js +1 -1
  61. package/test/workspaceBootstrapContributor.test.js +7 -6
  62. package/test/workspaceCrud.test.js +23 -0
  63. package/test/workspaceInvitesRepository.test.js +9 -8
  64. package/test/workspaceMembersService.test.js +131 -28
  65. package/test/workspacePendingInvitationsResource.test.js +2 -2
  66. package/test/workspacePendingInvitationsService.test.js +69 -22
  67. package/test/workspaceService.test.js +58 -47
  68. package/test/workspaceSettingsActions.test.js +14 -7
  69. package/test/workspaceSettingsRepository.test.js +11 -0
  70. package/test/workspaceSettingsService.test.js +11 -9
  71. package/test/workspacesRepository.test.js +16 -16
  72. package/test/workspacesRouteRequestInputValidator.test.js +4 -4
  73. package/src/server/common/repositories/repositoryUtils.js +0 -43
  74. package/src/server/common/support/deepFreeze.js +0 -1
  75. package/src/server/common/support/resolveActionUser.js +0 -9
@@ -1,4 +1,4 @@
1
- import { requireServiceMethod } from "@jskit-ai/kernel/server/actions/actionContributorHelpers";
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 "../support/resolveActionUser.js";
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
- function normalizeWorkspaceSurfaceIds(surfaceIds = []) {
17
- const source = Array.isArray(surfaceIds) ? surfaceIds : [];
18
- const normalized = new Set();
19
-
20
- for (const entry of source) {
21
- const surfaceId = normalizeSurfaceId(entry);
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 = normalizeWorkspaceSurfaceIds(
38
- workspaceMembershipOptionalSurfaceIds
39
- );
40
- const workspaceSurfaceIdSet = normalizeWorkspaceSurfaceIds(workspaceSurfaceIds);
29
+ const workspaceMembershipOptionalSurfaceIdSet = new Set(workspaceMembershipOptionalSurfaceIds);
30
+ const workspaceSurfaceIdSet = new Set(workspaceSurfaceIds);
41
31
 
42
- requireServiceMethod(workspaceService, "resolveWorkspaceContextForUserBySlug", contributorId);
43
-
44
- return Object.freeze({
45
- contributorId,
46
- async contribute({ definition = null, input, context } = {}) {
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
- const resolveOptions = { request };
67
- if (workspaceMembershipOptionalSurfaceIdSet.has(activeSurfaceId)) {
68
- resolveOptions.requireMembership = false;
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
- const resolvedWorkspaceContext = await workspaceService.resolveWorkspaceContextForUserBySlug(
72
- resolveActionUser(context, payload),
73
- payload.workspaceSlug,
74
- resolveOptions
75
- );
52
+ const resolveOptions = { request };
53
+ if (workspaceMembershipOptionalSurfaceIdSet.has(activeSurfaceId)) {
54
+ resolveOptions.requireMembership = false;
55
+ }
76
56
 
77
- const contribution = {
78
- requestMeta: {
79
- resolvedWorkspaceContext
80
- }
81
- };
57
+ const resolvedWorkspaceContext = await workspaceService.resolveWorkspaceContextForUserBySlug(
58
+ resolveActionUser(context, payload),
59
+ payload.workspaceSlug,
60
+ resolveOptions
61
+ );
82
62
 
83
- if (!context?.workspace) {
84
- contribution.workspace = resolvedWorkspaceContext.workspace;
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
- return contribution;
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
- function createWorkspaceAuthPolicyContextResolver({
5
- workspaceService,
6
- workspaceMembershipOptionalSurfaceIds = []
7
- } = {}) {
8
- if (!workspaceService || typeof workspaceService.resolveWorkspaceContextForUserBySlug !== "function") {
9
- throw new Error(
10
- "workspace auth policy context resolver requires workspaceService.resolveWorkspaceContextForUserBySlug()."
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
- if (
25
+ const requireMembership = !(
35
26
  (contextPolicy === "optional" && !permission) ||
36
27
  membershipOptionalSurfaceIds.has(activeSurfaceId)
37
- ) {
38
- resolveOptions.requireMembership = false;
39
- }
28
+ );
40
29
 
41
- const resolvedWorkspaceContext = await workspaceService.resolveWorkspaceContextForUserBySlug(
30
+ const { workspace, membership, permissions } = await workspaceService.resolveWorkspaceContextForUserBySlug(
42
31
  actor,
43
32
  workspaceSlug,
44
- resolveOptions
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 { normalizeOpaqueId, normalizeRecordId, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
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
- function createWorkspaceRouteVisibilityResolver({
22
- workspaceService,
23
- workspaceMembershipOptionalSurfaceIds = []
24
- } = {}) {
25
- if (!workspaceService || typeof workspaceService.resolveWorkspaceContextForUserBySlug !== "function") {
26
- throw new Error("workspace route visibility resolver requires workspaceService.resolveWorkspaceContextForUserBySlug().");
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
- if (!workspaceSlug || !actor) {
51
- return visibility === "workspace_user"
52
- ? buildVisibilityContribution({
53
- visibility,
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
- const activeSurfaceId = normalizeSurfaceId(
60
- request?.routeOptions?.config?.surface || context?.surface
61
- );
62
- const resolvedWorkspaceContext = await workspaceService.resolveWorkspaceContextForUserBySlug(
63
- actor,
64
- workspaceSlug,
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
- return buildVisibilityContribution({
81
- visibility,
82
- scopeOwnerId: resolvedWorkspaceOwnerId,
83
- userId
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 { resolveWorkspaceThemePalettes } from "@jskit-ai/workspaces-core/shared/settings";
2
- import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
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
- function mapWorkspaceSettingsPublic(workspaceSettings, { workspaceInvitationsEnabled = true } = {}) {
20
- const source = workspaceSettings && typeof workspaceSettings === "object" ? workspaceSettings : {};
21
- const invitesAvailable = workspaceInvitationsEnabled === true;
22
- const invitesEnabled = invitesAvailable && source.invitesEnabled !== false;
23
- const themePalettes = resolveWorkspaceThemePalettes(source);
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: invitesAvailable && invitesEnabled
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
- normalizeLowerText,
4
- normalizeRecordId,
2
+ isLostDuplicateInsertRace,
3
+ joinOrOpenTransaction,
5
4
  normalizeDbRecordId,
6
- normalizeText,
7
- isDuplicateEntryError,
8
- toIsoString,
9
- retryDeadlocks
10
- } from "./repositoryUtils.js";
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?.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
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
- function createRepository({ api } = {}) {
81
- if (!api?.resources?.workspaceInvites) {
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?.trx || null,
131
+ transaction: resolveTransaction(options),
133
132
  format: "plain"
134
133
  },
135
- createJsonRestContext(options?.context || null)
134
+ createJsonRestContext(options.context)
136
135
  )
137
136
  );
138
137
  }
139
138
 
140
139
  async function lockWorkspaceForInvitations(workspaceId, options = {}) {
141
- if (typeof options.trx !== "function" || options.trx.isCompleted?.()) {
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 = {}, options = {}) {
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?.trx || null
265
+ transaction: resolveTransaction(options)
266
266
  },
267
- createJsonRestContext(options?.context || null)
267
+ createJsonRestContext(options.context)
268
268
  );
269
269
 
270
270
  return normalizeInviteRecord(created);
271
271
  } catch (error) {
272
- if (options?.trx || error?.transactionOutcome !== "rolledBack" || !isDuplicateEntryError(error)) {
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
- if (!options.trx) {
297
- return withTransaction((trx) => expirePendingByWorkspaceIdAndEmail(workspaceId, email, { ...options, trx }));
298
- }
299
- await lockWorkspaceForInvitations(normalizedWorkspaceId, options);
300
- const patch = normalizeInvitePatchPayload({ status: "expired" });
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
- id: row.id,
317
- data: {
318
- status: patch.status,
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
- createJsonRestContext(options?.context || null)
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
- if (!options.trx) {
346
- return withTransaction((trx) => transitionPendingById(id, status, { ...options, trx, workspaceId }));
347
- }
348
- const invite = await findPendingByIdForWorkspace(id, workspaceId, { ...options, lock: true });
349
- if (!invite) {
350
- throw new AppError(404, "Invitation not found or already handled.", {
351
- code: "workspaces.invite.invitationNotFoundOrHandled"
352
- });
353
- }
354
- const updatedAt = new Date().toISOString();
355
- await api.resources.workspaceInvites.patch(
356
- {
357
- id,
358
- data: {
359
- status,
360
- [status === "accepted" ? "acceptedAt" : "revokedAt"]: updatedAt,
361
- updatedAt
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
- format: "plain",
364
- returning: "full",
365
- transaction: options.trx
366
- },
367
- createJsonRestContext(options.context || null)
368
- );
365
+ createJsonRestContext(options.context)
366
+ );
367
+ });
369
368
  }
370
369
 
371
370
  async function markAcceptedById(inviteId, options = {}) {