@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.
- 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,12 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
normalizeRecordId,
|
|
2
|
+
isLostDuplicateInsertRace,
|
|
4
3
|
normalizeDbRecordId,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
toIsoString
|
|
8
|
-
|
|
9
|
-
} from "
|
|
4
|
+
resolveTransaction,
|
|
5
|
+
retryDeadlocks,
|
|
6
|
+
toIsoString
|
|
7
|
+
} from "@jskit-ai/database-runtime/shared";
|
|
8
|
+
import { normalizeLowerText, normalizeRecordId, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
10
9
|
import {
|
|
11
10
|
createJsonRestContext,
|
|
12
11
|
extractJsonRestCollectionRows
|
|
@@ -29,20 +28,6 @@ function normalizeMembershipRecord(payload) {
|
|
|
29
28
|
};
|
|
30
29
|
}
|
|
31
30
|
|
|
32
|
-
function normalizeMembershipPatchPayload(payload = {}) {
|
|
33
|
-
const source = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
34
|
-
const normalized = {};
|
|
35
|
-
|
|
36
|
-
if (Object.hasOwn(source, "roleSid")) {
|
|
37
|
-
normalized.roleSid = normalizeLowerText(source.roleSid);
|
|
38
|
-
}
|
|
39
|
-
if (Object.hasOwn(source, "status")) {
|
|
40
|
-
normalized.status = normalizeLowerText(source.status);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return normalized;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
31
|
function normalizeMemberSummaryRow(row) {
|
|
47
32
|
if (!row) {
|
|
48
33
|
return null;
|
|
@@ -57,11 +42,8 @@ function normalizeMemberSummaryRow(row) {
|
|
|
57
42
|
};
|
|
58
43
|
}
|
|
59
44
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
throw new TypeError("workspaceMembershipsRepository requires json-rest-api workspaceMemberships resource.");
|
|
63
|
-
}
|
|
64
|
-
|
|
45
|
+
/** @param {{ api: import("@jskit-ai/json-rest-api-core/server/types").RuntimeJsonRestApi }} dependencies */
|
|
46
|
+
function createRepository({ api }) {
|
|
65
47
|
const withTransaction = (work) => retryDeadlocks(() => api.transaction(work));
|
|
66
48
|
|
|
67
49
|
async function queryMemberships(filters = {}, options = {}, { include = [] } = {}) {
|
|
@@ -79,10 +61,10 @@ function createRepository({ api } = {}) {
|
|
|
79
61
|
filters,
|
|
80
62
|
...(normalizedInclude.length > 0 ? { include: normalizedInclude } : {})
|
|
81
63
|
},
|
|
82
|
-
transaction: options
|
|
64
|
+
transaction: resolveTransaction(options),
|
|
83
65
|
format: "plain"
|
|
84
66
|
},
|
|
85
|
-
createJsonRestContext(options
|
|
67
|
+
createJsonRestContext(options.context)
|
|
86
68
|
)
|
|
87
69
|
);
|
|
88
70
|
}
|
|
@@ -127,9 +109,9 @@ function createRepository({ api } = {}) {
|
|
|
127
109
|
},
|
|
128
110
|
format: "plain",
|
|
129
111
|
returning: "full",
|
|
130
|
-
transaction: options
|
|
112
|
+
transaction: resolveTransaction(options)
|
|
131
113
|
},
|
|
132
|
-
createJsonRestContext(options
|
|
114
|
+
createJsonRestContext(options.context)
|
|
133
115
|
);
|
|
134
116
|
}
|
|
135
117
|
return findByWorkspaceIdAndUserId(normalizedWorkspaceId, normalizedUserId, options);
|
|
@@ -149,12 +131,12 @@ function createRepository({ api } = {}) {
|
|
|
149
131
|
},
|
|
150
132
|
format: "plain",
|
|
151
133
|
returning: "full",
|
|
152
|
-
transaction: options
|
|
134
|
+
transaction: resolveTransaction(options)
|
|
153
135
|
},
|
|
154
|
-
createJsonRestContext(options
|
|
136
|
+
createJsonRestContext(options.context)
|
|
155
137
|
);
|
|
156
138
|
} catch (error) {
|
|
157
|
-
if (
|
|
139
|
+
if (!isLostDuplicateInsertRace(error, options)) {
|
|
158
140
|
throw error;
|
|
159
141
|
}
|
|
160
142
|
}
|
|
@@ -162,7 +144,7 @@ function createRepository({ api } = {}) {
|
|
|
162
144
|
return findByWorkspaceIdAndUserId(normalizedWorkspaceId, normalizedUserId, options);
|
|
163
145
|
}
|
|
164
146
|
|
|
165
|
-
async function upsertMembership(workspaceId, userId, patch
|
|
147
|
+
async function upsertMembership(workspaceId, userId, patch, options = {}) {
|
|
166
148
|
const normalizedWorkspaceId = normalizeRecordId(workspaceId, { fallback: null });
|
|
167
149
|
const normalizedUserId = normalizeRecordId(userId, { fallback: null });
|
|
168
150
|
if (!normalizedWorkspaceId || !normalizedUserId) {
|
|
@@ -170,12 +152,8 @@ function createRepository({ api } = {}) {
|
|
|
170
152
|
}
|
|
171
153
|
|
|
172
154
|
const existing = await findByWorkspaceIdAndUserId(normalizedWorkspaceId, normalizedUserId, options);
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
status: patch?.status ?? existing?.status ?? "active"
|
|
176
|
-
});
|
|
177
|
-
const roleSid = normalizeLowerText(normalizedPatch.roleSid || "member") || "member";
|
|
178
|
-
const status = normalizeLowerText(normalizedPatch.status || "active") || "active";
|
|
155
|
+
const roleSid = normalizeLowerText(patch?.roleSid ?? existing?.roleSid) || "member";
|
|
156
|
+
const status = normalizeLowerText(patch?.status ?? existing?.status) || "active";
|
|
179
157
|
|
|
180
158
|
if (!existing) {
|
|
181
159
|
try {
|
|
@@ -192,12 +170,12 @@ function createRepository({ api } = {}) {
|
|
|
192
170
|
},
|
|
193
171
|
format: "plain",
|
|
194
172
|
returning: "full",
|
|
195
|
-
transaction: options
|
|
173
|
+
transaction: resolveTransaction(options)
|
|
196
174
|
},
|
|
197
|
-
createJsonRestContext(options
|
|
175
|
+
createJsonRestContext(options.context)
|
|
198
176
|
);
|
|
199
177
|
} catch (error) {
|
|
200
|
-
if (
|
|
178
|
+
if (!isLostDuplicateInsertRace(error, options)) {
|
|
201
179
|
throw error;
|
|
202
180
|
}
|
|
203
181
|
}
|
|
@@ -215,9 +193,9 @@ function createRepository({ api } = {}) {
|
|
|
215
193
|
},
|
|
216
194
|
format: "plain",
|
|
217
195
|
returning: "full",
|
|
218
|
-
transaction: options
|
|
196
|
+
transaction: resolveTransaction(options)
|
|
219
197
|
},
|
|
220
|
-
createJsonRestContext(options
|
|
198
|
+
createJsonRestContext(options.context)
|
|
221
199
|
);
|
|
222
200
|
|
|
223
201
|
return findByWorkspaceIdAndUserId(normalizedWorkspaceId, normalizedUserId, options);
|
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
isDuplicateEntryError,
|
|
4
|
-
normalizeText,
|
|
5
|
-
toIsoString,
|
|
6
|
-
retryDeadlocks
|
|
7
|
-
} from "./repositoryUtils.js";
|
|
1
|
+
import { isLostDuplicateInsertRace, resolveTransaction, retryDeadlocks, toIsoString } from "@jskit-ai/database-runtime/shared";
|
|
2
|
+
import { normalizeRecordId, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
8
3
|
import {
|
|
9
4
|
createJsonRestContext,
|
|
10
5
|
extractJsonRestCollectionRows
|
|
@@ -28,11 +23,8 @@ function normalizeWorkspaceRecord(payload = null) {
|
|
|
28
23
|
};
|
|
29
24
|
}
|
|
30
25
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
throw new TypeError("workspacesRepository requires json-rest-api workspaces and workspaceMemberships resources.");
|
|
34
|
-
}
|
|
35
|
-
|
|
26
|
+
/** @param {{ api: import("@jskit-ai/json-rest-api-core/server/types").RuntimeJsonRestApi }} dependencies */
|
|
27
|
+
function createRepository({ api }) {
|
|
36
28
|
const withTransaction = (work) => retryDeadlocks(() => api.transaction(work));
|
|
37
29
|
|
|
38
30
|
async function queryFirst(filters = {}, options = {}) {
|
|
@@ -42,10 +34,10 @@ function createRepository({ api } = {}) {
|
|
|
42
34
|
queryParams: {
|
|
43
35
|
filters
|
|
44
36
|
},
|
|
45
|
-
transaction: options
|
|
37
|
+
transaction: resolveTransaction(options),
|
|
46
38
|
format: "plain"
|
|
47
39
|
},
|
|
48
|
-
createJsonRestContext(options
|
|
40
|
+
createJsonRestContext(options.context)
|
|
49
41
|
)
|
|
50
42
|
);
|
|
51
43
|
|
|
@@ -85,10 +77,10 @@ function createRepository({ api } = {}) {
|
|
|
85
77
|
isPersonal: true
|
|
86
78
|
}
|
|
87
79
|
},
|
|
88
|
-
transaction: options
|
|
80
|
+
transaction: resolveTransaction(options),
|
|
89
81
|
format: "plain"
|
|
90
82
|
},
|
|
91
|
-
createJsonRestContext(options
|
|
83
|
+
createJsonRestContext(options.context)
|
|
92
84
|
)
|
|
93
85
|
);
|
|
94
86
|
|
|
@@ -104,7 +96,7 @@ function createRepository({ api } = {}) {
|
|
|
104
96
|
return normalizedRows[0] || null;
|
|
105
97
|
}
|
|
106
98
|
|
|
107
|
-
async function insert(payload
|
|
99
|
+
async function insert(payload, options = {}) {
|
|
108
100
|
const source = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
109
101
|
const ownerUserId = normalizeRecordId(source.ownerUserId, { fallback: null });
|
|
110
102
|
if (!ownerUserId) {
|
|
@@ -130,14 +122,14 @@ function createRepository({ api } = {}) {
|
|
|
130
122
|
},
|
|
131
123
|
format: "plain",
|
|
132
124
|
returning: "full",
|
|
133
|
-
transaction: options
|
|
125
|
+
transaction: resolveTransaction(options)
|
|
134
126
|
},
|
|
135
|
-
createJsonRestContext(options
|
|
127
|
+
createJsonRestContext(options.context)
|
|
136
128
|
);
|
|
137
129
|
|
|
138
130
|
return normalizeWorkspaceRecord(created);
|
|
139
131
|
} catch (error) {
|
|
140
|
-
if (
|
|
132
|
+
if (!isLostDuplicateInsertRace(error, options)) {
|
|
141
133
|
throw error;
|
|
142
134
|
}
|
|
143
135
|
if (!Object.hasOwn(createPayload, "slug")) {
|
|
@@ -152,7 +144,7 @@ function createRepository({ api } = {}) {
|
|
|
152
144
|
}
|
|
153
145
|
}
|
|
154
146
|
|
|
155
|
-
async function updateById(workspaceId, patch
|
|
147
|
+
async function updateById(workspaceId, patch, options = {}) {
|
|
156
148
|
const normalizedWorkspaceId = normalizeRecordId(workspaceId, { fallback: null });
|
|
157
149
|
if (!normalizedWorkspaceId) {
|
|
158
150
|
return null;
|
|
@@ -174,9 +166,9 @@ function createRepository({ api } = {}) {
|
|
|
174
166
|
data: workspacePatch,
|
|
175
167
|
format: "plain",
|
|
176
168
|
returning: "full",
|
|
177
|
-
transaction: options
|
|
169
|
+
transaction: resolveTransaction(options)
|
|
178
170
|
},
|
|
179
|
-
createJsonRestContext(options
|
|
171
|
+
createJsonRestContext(options.context)
|
|
180
172
|
);
|
|
181
173
|
|
|
182
174
|
return normalizeWorkspaceRecord(updated);
|
|
@@ -198,10 +190,10 @@ function createRepository({ api } = {}) {
|
|
|
198
190
|
},
|
|
199
191
|
include: ["workspace"]
|
|
200
192
|
},
|
|
201
|
-
transaction: options
|
|
193
|
+
transaction: resolveTransaction(options),
|
|
202
194
|
format: "plain"
|
|
203
195
|
},
|
|
204
|
-
createJsonRestContext(options
|
|
196
|
+
createJsonRestContext(options.context)
|
|
205
197
|
)
|
|
206
198
|
);
|
|
207
199
|
|
package/src/server/common/services/{workspaceContextService.js → workspaceDirectoryService.js}
RENAMED
|
@@ -1,18 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { returnJsonApiData } from "@jskit-ai/http-runtime/shared";
|
|
2
|
+
import { pickOwnProperties } from "@jskit-ai/kernel/shared/support";
|
|
2
3
|
import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
} from "../../../shared/tenancyProfile.js";
|
|
7
|
-
import {
|
|
8
|
-
resolveRolePermissions,
|
|
9
|
-
createWorkspaceRoleCatalog
|
|
10
|
-
} from "../../../shared/roles.js";
|
|
11
|
-
import {
|
|
12
|
-
mapWorkspaceSummary
|
|
13
|
-
} from "../formatters/workspaceFormatter.js";
|
|
14
|
-
import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
15
|
-
import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
|
|
4
|
+
import { normalizeLowerText, normalizeRecordId, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
5
|
+
import { WORKSPACE_PATCH_FIELD_KEYS } from "../../../shared/resources/workspaceResource.js";
|
|
6
|
+
import { listCatalogRolePermissions } from "../../../shared/roles.js";
|
|
7
|
+
import { TENANCY_MODE_NONE } from "../../../shared/tenancyProfile.js";
|
|
8
|
+
import { mapWorkspaceSummary } from "../formatters/workspaceFormatter.js";
|
|
16
9
|
|
|
17
10
|
function toSlugPart(value) {
|
|
18
11
|
const normalized = normalizeLowerText(value)
|
|
@@ -22,7 +15,7 @@ function toSlugPart(value) {
|
|
|
22
15
|
return normalized || "workspace";
|
|
23
16
|
}
|
|
24
17
|
|
|
25
|
-
function buildWorkspaceBaseSlug(user
|
|
18
|
+
function buildWorkspaceBaseSlug(user) {
|
|
26
19
|
const username = normalizeLowerText(user.username);
|
|
27
20
|
if (username) {
|
|
28
21
|
return toSlugPart(username);
|
|
@@ -38,7 +31,12 @@ function buildWorkspaceBaseSlug(user = {}) {
|
|
|
38
31
|
return "workspace";
|
|
39
32
|
}
|
|
40
33
|
|
|
41
|
-
|
|
34
|
+
/**
|
|
35
|
+
* The `workspaces.personal-workspace-name` default: the user's display name or
|
|
36
|
+
* email, followed by "'s Workspace".
|
|
37
|
+
* @type {import("../../types.js").PersonalWorkspaceName}
|
|
38
|
+
*/
|
|
39
|
+
function buildWorkspaceName(user) {
|
|
42
40
|
const displayName = normalizeText(user.displayName);
|
|
43
41
|
if (displayName) {
|
|
44
42
|
return `${displayName}'s Workspace`;
|
|
@@ -50,42 +48,26 @@ function buildWorkspaceName(user = {}) {
|
|
|
50
48
|
return "Workspace";
|
|
51
49
|
}
|
|
52
50
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function normalizeWorkspaceCreationInput(payload = {}) {
|
|
63
|
-
const source = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
64
|
-
return {
|
|
65
|
-
name: normalizeText(source.name),
|
|
66
|
-
requestedSlug: normalizeLowerText(source.slug)
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
51
|
+
/**
|
|
52
|
+
* @param {object} dependencies
|
|
53
|
+
* @param {import("../../types.js").WorkspaceTenancyProfile} dependencies.tenancyProfile
|
|
54
|
+
* @param {import("../../types.js").WorkspaceRoleCatalog} dependencies.roleCatalog
|
|
55
|
+
* @param {import("../../types.js").PersonalWorkspaceName} dependencies.personalWorkspaceName
|
|
56
|
+
* @param {import("../../types.js").WorkspacesRepository} dependencies.workspacesRepository
|
|
57
|
+
* @param {import("../../types.js").WorkspaceMembershipsRepository} dependencies.workspaceMembershipsRepository
|
|
58
|
+
* @param {import("../../types.js").WorkspaceSettingsRepository} dependencies.workspaceSettingsRepository
|
|
59
|
+
*/
|
|
70
60
|
function createService({
|
|
71
|
-
|
|
61
|
+
tenancyProfile,
|
|
62
|
+
roleCatalog,
|
|
63
|
+
personalWorkspaceName,
|
|
72
64
|
workspacesRepository,
|
|
73
65
|
workspaceMembershipsRepository,
|
|
74
66
|
workspaceSettingsRepository
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
!workspaceMembershipsRepository ||
|
|
79
|
-
!workspaceSettingsRepository
|
|
80
|
-
) {
|
|
81
|
-
throw new Error("workspaceService requires repositories.");
|
|
82
|
-
}
|
|
67
|
+
}) {
|
|
68
|
+
const resolvedTenancyMode = tenancyProfile.mode;
|
|
69
|
+
const workspacePolicy = tenancyProfile.workspace;
|
|
83
70
|
|
|
84
|
-
const resolvedTenancyProfile = resolveTenancyProfile(appConfig);
|
|
85
|
-
const resolvedTenancyMode = resolvedTenancyProfile.mode;
|
|
86
|
-
const workspacePolicy = resolvedTenancyProfile.workspace;
|
|
87
|
-
const personalWorkspaceName =
|
|
88
|
-
typeof appConfig.personalWorkspaceName === "function" ? appConfig.personalWorkspaceName : null;
|
|
89
71
|
async function ensureUniqueWorkspaceSlug(baseSlug, options = {}) {
|
|
90
72
|
let suffix = 0;
|
|
91
73
|
while (suffix < 1000) {
|
|
@@ -96,11 +78,11 @@ function createService({
|
|
|
96
78
|
return candidate;
|
|
97
79
|
}
|
|
98
80
|
}
|
|
99
|
-
throw new
|
|
81
|
+
throw new Error("Unable to generate unique workspace slug.");
|
|
100
82
|
}
|
|
101
83
|
|
|
102
84
|
async function ensureWorkspaceSettingsForWorkspace(workspace, options = {}) {
|
|
103
|
-
return workspaceSettingsRepository.ensureForWorkspaceId(workspace
|
|
85
|
+
return workspaceSettingsRepository.ensureForWorkspaceId(workspace.id, {
|
|
104
86
|
...options,
|
|
105
87
|
workspace
|
|
106
88
|
});
|
|
@@ -123,7 +105,7 @@ function createService({
|
|
|
123
105
|
const inserted = await workspacesRepository.insert(
|
|
124
106
|
{
|
|
125
107
|
slug,
|
|
126
|
-
name: normalizeText(personalWorkspaceName
|
|
108
|
+
name: normalizeText(personalWorkspaceName(user)) || buildWorkspaceName(user),
|
|
127
109
|
ownerUserId: normalizedUserId,
|
|
128
110
|
isPersonal: true,
|
|
129
111
|
avatarUrl: ""
|
|
@@ -147,7 +129,6 @@ function createService({
|
|
|
147
129
|
}
|
|
148
130
|
|
|
149
131
|
const list = await workspacesRepository.listForUserId(normalizedUserId, options);
|
|
150
|
-
const roleCatalog = createWorkspaceRoleCatalog(appConfig);
|
|
151
132
|
const accessible = list
|
|
152
133
|
.map((entry) => mapWorkspaceSummary(
|
|
153
134
|
entry,
|
|
@@ -160,7 +141,7 @@ function createService({
|
|
|
160
141
|
}
|
|
161
142
|
|
|
162
143
|
async function listWorkspacesForAuthenticatedUser(user, options = {}) {
|
|
163
|
-
return listWorkspacesForUser(user, options);
|
|
144
|
+
return returnJsonApiData({ items: await listWorkspacesForUser(user, options), nextCursor: null });
|
|
164
145
|
}
|
|
165
146
|
|
|
166
147
|
async function ensureProvisionedWorkspaceForAuthenticatedUser(user, options = {}) {
|
|
@@ -179,7 +160,14 @@ function createService({
|
|
|
179
160
|
}, options);
|
|
180
161
|
}
|
|
181
162
|
|
|
182
|
-
|
|
163
|
+
/**
|
|
164
|
+
* Creates a workspace the user owns. The action's input schema has already
|
|
165
|
+
* trimmed the name and slug and lowercased the slug.
|
|
166
|
+
* @param {{ id?: unknown } | null} user
|
|
167
|
+
* @param {{ name: string, slug?: string }} input
|
|
168
|
+
* @param {object} [options]
|
|
169
|
+
*/
|
|
170
|
+
async function createWorkspaceForAuthenticatedUser(user, { name, slug: requestedSlug }, options = {}) {
|
|
183
171
|
const normalizedUserId = normalizeRecordId(user?.id, { fallback: null });
|
|
184
172
|
if (!normalizedUserId) {
|
|
185
173
|
throw new AppError(401, "Authentication required.", { code: "workspaces.auth.required" });
|
|
@@ -191,17 +179,11 @@ function createService({
|
|
|
191
179
|
});
|
|
192
180
|
}
|
|
193
181
|
|
|
194
|
-
const
|
|
195
|
-
if (!createInput.name) {
|
|
196
|
-
throw new AppError(400, "Workspace name is required.", { code: "workspaces.workspace.nameRequired" });
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const slugBase = createInput.requestedSlug || toSlugPart(createInput.name);
|
|
200
|
-
const slug = await ensureUniqueWorkspaceSlug(slugBase, options);
|
|
182
|
+
const slug = await ensureUniqueWorkspaceSlug(requestedSlug || toSlugPart(name), options);
|
|
201
183
|
const inserted = await workspacesRepository.insert(
|
|
202
184
|
{
|
|
203
185
|
slug,
|
|
204
|
-
name
|
|
186
|
+
name,
|
|
205
187
|
ownerUserId: normalizedUserId,
|
|
206
188
|
isPersonal: false,
|
|
207
189
|
avatarUrl: ""
|
|
@@ -211,21 +193,23 @@ function createService({
|
|
|
211
193
|
|
|
212
194
|
await workspaceMembershipsRepository.ensureOwnerMembership(inserted.id, normalizedUserId, options);
|
|
213
195
|
await ensureWorkspaceSettingsForWorkspace(inserted, options);
|
|
214
|
-
return inserted;
|
|
196
|
+
return returnJsonApiData(inserted);
|
|
215
197
|
}
|
|
216
198
|
|
|
217
199
|
async function getWorkspaceForAuthenticatedUser(user, workspaceSlug, options = {}) {
|
|
218
200
|
const workspaceContext = await resolveWorkspaceContextForUserBySlug(user, workspaceSlug, options);
|
|
219
|
-
return workspaceContext.workspace;
|
|
201
|
+
return returnJsonApiData(workspaceContext.workspace);
|
|
220
202
|
}
|
|
221
203
|
|
|
222
|
-
|
|
204
|
+
/** The repository writes every field it receives, so only the patchable fields go to it. */
|
|
205
|
+
async function updateWorkspaceForAuthenticatedUser(user, workspaceSlug, input, options = {}) {
|
|
223
206
|
const workspaceContext = await resolveWorkspaceContextForUserBySlug(user, workspaceSlug, options);
|
|
224
|
-
|
|
207
|
+
const patch = pickOwnProperties(input, WORKSPACE_PATCH_FIELD_KEYS);
|
|
208
|
+
return returnJsonApiData(await workspacesRepository.updateById(workspaceContext.workspace.id, patch, options));
|
|
225
209
|
}
|
|
226
210
|
|
|
227
211
|
async function resolveWorkspaceContextForUserBySlug(user, workspaceSlug, options = {}) {
|
|
228
|
-
const requireMembership = options
|
|
212
|
+
const requireMembership = options.requireMembership !== false;
|
|
229
213
|
const normalizedUserId = normalizeRecordId(user?.id, { fallback: null });
|
|
230
214
|
if (!normalizedUserId) {
|
|
231
215
|
throw new AppError(401, "Authentication required.", { code: "workspaces.auth.required" });
|
|
@@ -269,7 +253,7 @@ function createService({
|
|
|
269
253
|
}
|
|
270
254
|
|
|
271
255
|
const workspaceSettings = await ensureWorkspaceSettingsForWorkspace(workspace, options);
|
|
272
|
-
const permissions = membership ?
|
|
256
|
+
const permissions = membership ? listCatalogRolePermissions(roleCatalog, membership.roleSid) : [];
|
|
273
257
|
|
|
274
258
|
return {
|
|
275
259
|
workspace,
|
|
@@ -280,10 +264,6 @@ function createService({
|
|
|
280
264
|
}
|
|
281
265
|
|
|
282
266
|
return Object.freeze({
|
|
283
|
-
toSlugPart,
|
|
284
|
-
buildWorkspaceName,
|
|
285
|
-
buildWorkspaceBaseSlug,
|
|
286
|
-
hashInviteToken,
|
|
287
267
|
ensurePersonalWorkspaceForUser,
|
|
288
268
|
ensureProvisionedWorkspaceForAuthenticatedUser,
|
|
289
269
|
createWorkspaceForAuthenticatedUser,
|
|
@@ -295,4 +275,4 @@ function createService({
|
|
|
295
275
|
});
|
|
296
276
|
}
|
|
297
277
|
|
|
298
|
-
export { createService };
|
|
278
|
+
export { buildWorkspaceName, createService };
|
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
import { normalizeDbRecordId } from "@jskit-ai/database-runtime/shared";
|
|
2
|
+
import { jsonApiResultValue } from "@jskit-ai/http-runtime/shared";
|
|
2
3
|
import { createEntityChangedActionEvent } from "@jskit-ai/kernel/server/actions";
|
|
4
|
+
import { resolveActorId } from "@jskit-ai/kernel/server/actions/actionContributorHelpers";
|
|
3
5
|
import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
|
|
4
6
|
import { resolveWorkspace } from "../../support/resolveWorkspace.js";
|
|
5
7
|
|
|
6
|
-
function
|
|
7
|
-
return
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function actorId({ context } = {}) {
|
|
11
|
-
return context?.actor?.id;
|
|
8
|
+
function actorId({ context }) {
|
|
9
|
+
return resolveActorId(context);
|
|
12
10
|
}
|
|
13
11
|
|
|
14
12
|
function workspaceId(execution = {}) {
|
|
15
|
-
return resolveWorkspace(execution.context, execution.input)?.id ||
|
|
13
|
+
return resolveWorkspace(execution.context, execution.input)?.id || jsonApiResultValue(execution.result)?.workspaceId;
|
|
16
14
|
}
|
|
17
15
|
|
|
18
16
|
function workspaceSlugPayload(execution = {}) {
|
|
@@ -25,8 +23,8 @@ function workspaceSlugPayload(execution = {}) {
|
|
|
25
23
|
|
|
26
24
|
const INVITE_RECIPIENT_BOOTSTRAP_AUDIENCE = Object.freeze({
|
|
27
25
|
preset: "event_scope",
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
/** @param {{ knex: import("@jskit-ai/database-runtime/server/types").RuntimeKnex, event: { entityId?: unknown } }} input */
|
|
27
|
+
async userQuery({ knex, event }) {
|
|
30
28
|
const inviteId = normalizeRecordId(event?.entityId, { fallback: null });
|
|
31
29
|
if (!inviteId) return [];
|
|
32
30
|
const row = await knex("workspace_invites as wi")
|
|
@@ -38,13 +36,21 @@ const INVITE_RECIPIENT_BOOTSTRAP_AUDIENCE = Object.freeze({
|
|
|
38
36
|
}
|
|
39
37
|
});
|
|
40
38
|
|
|
39
|
+
/**
|
|
40
|
+
* @param {object} event
|
|
41
|
+
* @param {string} event.workspaceEntity
|
|
42
|
+
* @param {string} event.workspaceOperation
|
|
43
|
+
* @param {string} event.workspaceRealtimeEvent
|
|
44
|
+
* @param {typeof workspaceId} [event.bootstrapEntityId]
|
|
45
|
+
* @param {string} [event.bootstrapAudience]
|
|
46
|
+
*/
|
|
41
47
|
function createWorkspaceEntityAndBootstrapEvents({
|
|
42
48
|
workspaceEntity,
|
|
43
49
|
workspaceOperation,
|
|
44
50
|
workspaceRealtimeEvent,
|
|
45
51
|
bootstrapEntityId = workspaceId,
|
|
46
52
|
bootstrapAudience = "event_scope"
|
|
47
|
-
}
|
|
53
|
+
}) {
|
|
48
54
|
return Object.freeze([
|
|
49
55
|
createEntityChangedActionEvent({
|
|
50
56
|
source: "workspace",
|
|
@@ -70,6 +76,7 @@ function createWorkspaceEntityAndBootstrapEvents({
|
|
|
70
76
|
]);
|
|
71
77
|
}
|
|
72
78
|
|
|
79
|
+
/** @param {{ source: string, entity: string, realtimeEvent: string }} event */
|
|
73
80
|
function createActorEvent({ source, entity, realtimeEvent }) {
|
|
74
81
|
return createEntityChangedActionEvent({
|
|
75
82
|
source,
|
|
@@ -80,15 +87,16 @@ function createActorEvent({ source, entity, realtimeEvent }) {
|
|
|
80
87
|
});
|
|
81
88
|
}
|
|
82
89
|
|
|
90
|
+
/** @param {{ entity: string, realtimeEvent: string }} event */
|
|
83
91
|
function createWorkspaceAudienceEvent({ entity, realtimeEvent }) {
|
|
84
92
|
return createEntityChangedActionEvent({
|
|
85
93
|
source: "workspace",
|
|
86
94
|
entity,
|
|
87
95
|
operation: "updated",
|
|
88
|
-
entityId: ({ result }) => result
|
|
96
|
+
entityId: ({ result }) => jsonApiResultValue(result).workspaceId,
|
|
89
97
|
realtime: {
|
|
90
98
|
event: realtimeEvent,
|
|
91
|
-
audience: ({ result }) => ({ workspaceId: result
|
|
99
|
+
audience: ({ result }) => ({ workspaceId: jsonApiResultValue(result).workspaceId })
|
|
92
100
|
}
|
|
93
101
|
});
|
|
94
102
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The JSON:API record id of a workspace response: the workspace's id, or the
|
|
3
|
+
* route's workspace slug.
|
|
4
|
+
* @param {{ id?: unknown, workspace?: { id?: unknown } }} record
|
|
5
|
+
* @param {{ request?: { params?: { workspaceSlug?: unknown } } }} context
|
|
6
|
+
*/
|
|
7
|
+
function resolveWorkspaceRecordId(record, context = {}) {
|
|
8
|
+
const workspaceId = record?.workspace?.id ?? record?.id;
|
|
9
|
+
if (workspaceId != null && String(workspaceId).trim()) {
|
|
10
|
+
return workspaceId;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const workspaceSlug = context.request?.params?.workspaceSlug;
|
|
14
|
+
if (workspaceSlug != null && String(workspaceSlug).trim()) {
|
|
15
|
+
return workspaceSlug;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
throw new Error("Workspace JSON:API response requires workspace id.");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { resolveWorkspaceRecordId };
|
|
@@ -37,9 +37,10 @@ const routeParamsValidator = deepFreeze({
|
|
|
37
37
|
mode: "patch"
|
|
38
38
|
});
|
|
39
39
|
|
|
40
|
+
// No mode: an action takes it as create, a route or a composition as patch; with
|
|
41
|
+
// no required field and no default, both validate it the same.
|
|
40
42
|
const workspaceSlugParamsValidator = deepFreeze({
|
|
41
|
-
schema: workspaceSlugParamsSchema
|
|
42
|
-
mode: "patch"
|
|
43
|
+
schema: workspaceSlugParamsSchema
|
|
43
44
|
});
|
|
44
45
|
|
|
45
46
|
export { routeParamsValidator, workspaceSlugParamsValidator };
|
package/src/server/i18n.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { createServerI18n } from "@jskit-ai/kernel/server/i18n";
|
|
2
2
|
import en from "../../locales/en.json" with { type: "json" };
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Workspace texts rendered on the server (invitation emails) in the recipient's
|
|
6
|
+
* language, from the app's translations under appRoot, or English.
|
|
7
|
+
* @param {{ appRoot?: import("@jskit-ai/kernel/server/types").RuntimeAppRoot }} [options]
|
|
8
|
+
*/
|
|
6
9
|
function createWorkspacesServerI18n({ appRoot = "" } = {}) {
|
|
7
10
|
return createServerI18n({ appRoot, catalogs: [en] });
|
|
8
11
|
}
|
|
@@ -1,16 +1,19 @@
|
|
|
1
|
+
import { resolveRequest } from "@jskit-ai/kernel/server/actions/actionContributorHelpers";
|
|
1
2
|
import { normalizeObject } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
/**
|
|
5
|
+
* The workspace an action runs in: the input's workspace, else the context's,
|
|
6
|
+
* else the one the workspace context contributor resolved, else the request's.
|
|
7
|
+
* This is json-rest-api-core's resolveWorkspaceScopeValue order.
|
|
8
|
+
* @param {object} context
|
|
9
|
+
* @param {unknown} [input]
|
|
10
|
+
*/
|
|
8
11
|
function resolveWorkspace(context = {}, input = {}) {
|
|
9
12
|
const payload = normalizeObject(input);
|
|
10
|
-
const requestMeta = normalizeObject(context
|
|
13
|
+
const requestMeta = normalizeObject(context.requestMeta);
|
|
11
14
|
const resolvedWorkspaceContext = normalizeObject(requestMeta.resolvedWorkspaceContext);
|
|
12
15
|
|
|
13
|
-
return payload.workspace ||
|
|
16
|
+
return payload.workspace || context.workspace || resolvedWorkspaceContext.workspace || resolveRequest(context)?.workspace || null;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
export { resolveWorkspace };
|