@jskit-ai/workspaces-core 0.1.14 → 0.1.15

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 (83) hide show
  1. package/package.descriptor.mjs +2 -2
  2. package/package.json +18 -3
  3. package/src/server/WorkspacesCoreServiceProvider.js +41 -2
  4. package/src/server/common/contributors/workspaceActionContextContributor.js +88 -0
  5. package/src/server/common/contributors/workspaceAuthPolicyContextResolver.js +34 -0
  6. package/src/server/common/contributors/workspaceRouteVisibilityResolver.js +78 -0
  7. package/src/server/common/formatters/workspaceFormatter.js +53 -0
  8. package/src/server/common/repositories/repositoryUtils.js +59 -0
  9. package/src/server/common/repositories/workspaceInvitesRepository.js +208 -0
  10. package/src/server/common/repositories/workspaceMembershipsRepository.js +190 -0
  11. package/src/server/common/repositories/workspacesRepository.js +202 -0
  12. package/src/server/common/services/workspaceContextService.js +281 -0
  13. package/src/server/common/support/deepFreeze.js +1 -0
  14. package/src/server/common/support/realtimeServiceEvents.js +91 -0
  15. package/src/server/common/support/resolveActionUser.js +9 -0
  16. package/src/server/common/support/workspaceRoutePaths.js +18 -0
  17. package/src/server/common/validators/authenticatedUserValidator.js +43 -0
  18. package/src/server/common/validators/routeParamsValidator.js +62 -0
  19. package/src/server/registerWorkspaceBootstrap.js +27 -0
  20. package/src/server/registerWorkspaceCore.js +100 -0
  21. package/src/server/registerWorkspaceRepositories.js +26 -0
  22. package/src/server/support/resolveWorkspace.js +16 -0
  23. package/src/server/support/workspaceActionSurfaces.js +118 -0
  24. package/src/server/support/workspaceInvitationsPolicy.js +45 -0
  25. package/src/server/support/workspaceRouteInput.js +22 -0
  26. package/src/server/workspaceBootstrapContributor.js +233 -0
  27. package/src/server/workspaceDirectory/bootWorkspaceDirectoryRoutes.js +133 -0
  28. package/src/server/workspaceDirectory/registerWorkspaceDirectory.js +19 -0
  29. package/src/server/workspaceDirectory/workspaceDirectoryActions.js +133 -0
  30. package/src/server/workspaceMembers/bootWorkspaceMembers.js +236 -0
  31. package/src/server/workspaceMembers/registerWorkspaceMembers.js +108 -0
  32. package/src/server/workspaceMembers/workspaceMembersActions.js +186 -0
  33. package/src/server/workspaceMembers/workspaceMembersService.js +222 -0
  34. package/src/server/workspacePendingInvitations/bootWorkspacePendingInvitations.js +62 -0
  35. package/src/server/workspacePendingInvitations/registerWorkspacePendingInvitations.js +119 -0
  36. package/src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js +74 -0
  37. package/src/server/workspacePendingInvitations/workspacePendingInvitationsService.js +138 -0
  38. package/src/server/workspaceSettings/bootWorkspaceSettings.js +76 -0
  39. package/src/server/workspaceSettings/registerWorkspaceSettings.js +62 -0
  40. package/src/server/workspaceSettings/workspaceSettingsActions.js +72 -0
  41. package/src/server/workspaceSettings/workspaceSettingsRepository.js +154 -0
  42. package/src/server/workspaceSettings/workspaceSettingsService.js +66 -0
  43. package/src/shared/operationMessages.js +16 -0
  44. package/src/shared/resources/resolveGlobalArrayRegistry.js +6 -0
  45. package/src/shared/resources/workspaceMembersResource.js +354 -0
  46. package/src/shared/resources/workspacePendingInvitationsResource.js +82 -0
  47. package/src/shared/resources/workspaceResource.js +176 -0
  48. package/src/shared/resources/workspaceSettingsFields.js +59 -0
  49. package/src/shared/resources/workspaceSettingsResource.js +169 -0
  50. package/src/shared/roles.js +161 -0
  51. package/src/shared/settings.js +119 -0
  52. package/src/shared/support/workspacePathModel.js +145 -0
  53. package/src/shared/tenancyMode.js +35 -0
  54. package/src/shared/tenancyProfile.js +73 -0
  55. package/templates/packages/main/src/shared/resources/workspaceSettingsFields.js +2 -2
  56. package/test/registerServiceRealtimeEvents.test.js +116 -0
  57. package/test/registerWorkspaceDirectory.test.js +31 -0
  58. package/test/registerWorkspaceSettings.test.js +40 -0
  59. package/test/repositoryContracts.test.js +34 -0
  60. package/test/resourcesCanonical.test.js +74 -0
  61. package/test/roles.test.js +159 -0
  62. package/test/routeParamsValidator.test.js +49 -0
  63. package/test/settingsFieldRegistriesSingleton.test.js +14 -0
  64. package/test/tenancyProfile.test.js +67 -0
  65. package/test/usersRouteResources.test.js +97 -0
  66. package/test/workspaceActionContextContributor.test.js +344 -0
  67. package/test/workspaceActionSurfaces.test.js +85 -0
  68. package/test/workspaceAuthPolicyContextResolver.test.js +119 -0
  69. package/test/workspaceBootstrapContributor.test.js +169 -0
  70. package/test/workspaceInvitationsPolicy.test.js +71 -0
  71. package/test/workspaceInvitesRepository.test.js +111 -0
  72. package/test/workspaceMembersService.test.js +398 -0
  73. package/test/workspacePathModel.test.js +93 -0
  74. package/test/workspacePendingInvitationsResource.test.js +38 -0
  75. package/test/workspacePendingInvitationsService.test.js +151 -0
  76. package/test/workspaceRouteVisibilityResolver.test.js +83 -0
  77. package/test/workspaceService.test.js +546 -0
  78. package/test/workspaceSettingsActions.test.js +52 -0
  79. package/test/workspaceSettingsRepository.test.js +202 -0
  80. package/test/workspaceSettingsResource.test.js +169 -0
  81. package/test/workspaceSettingsService.test.js +140 -0
  82. package/test/workspacesRouteRequestInputValidator.test.js +5 -5
  83. package/test-support/registerDefaultSettingsFields.js +1 -0
@@ -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.14",
4
+ version: "0.1.15",
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.48"
113
+ "@jskit-ai/users-core": "0.1.49"
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.14",
3
+ "version": "0.1.15",
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
- "@jskit-ai/users-core": "0.1.48"
21
+ "@fastify/type-provider-typebox": "^6.1.0",
22
+ "@jskit-ai/auth-core": "0.1.38",
23
+ "@jskit-ai/database-runtime": "0.1.39",
24
+ "@jskit-ai/http-runtime": "0.1.38",
25
+ "@jskit-ai/kernel": "0.1.39",
26
+ "@jskit-ai/users-core": "0.1.49",
27
+ "typebox": "^1.0.81"
13
28
  }
14
29
  }
@@ -1,5 +1,44 @@
1
- import { UsersWorkspacesServiceProvider } from "@jskit-ai/users-core/server/UsersWorkspacesServiceProvider";
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
- const WorkspacesCoreServiceProvider = UsersWorkspacesServiceProvider;
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 };