@jskit-ai/workspaces-core 0.1.14 → 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.descriptor.mjs +2 -2
- package/package.json +18 -3
- package/src/server/WorkspacesCoreServiceProvider.js +41 -2
- package/src/server/common/contributors/workspaceActionContextContributor.js +88 -0
- package/src/server/common/contributors/workspaceAuthPolicyContextResolver.js +34 -0
- package/src/server/common/contributors/workspaceRouteVisibilityResolver.js +78 -0
- package/src/server/common/formatters/workspaceFormatter.js +53 -0
- package/src/server/common/repositories/repositoryUtils.js +59 -0
- package/src/server/common/repositories/workspaceInvitesRepository.js +208 -0
- package/src/server/common/repositories/workspaceMembershipsRepository.js +190 -0
- package/src/server/common/repositories/workspacesRepository.js +202 -0
- package/src/server/common/services/workspaceContextService.js +281 -0
- package/src/server/common/support/deepFreeze.js +1 -0
- package/src/server/common/support/realtimeServiceEvents.js +91 -0
- package/src/server/common/support/resolveActionUser.js +9 -0
- package/src/server/common/support/workspaceRoutePaths.js +18 -0
- package/src/server/common/validators/authenticatedUserValidator.js +43 -0
- package/src/server/common/validators/routeParamsValidator.js +62 -0
- package/src/server/registerWorkspaceBootstrap.js +27 -0
- package/src/server/registerWorkspaceCore.js +100 -0
- package/src/server/registerWorkspaceRepositories.js +26 -0
- package/src/server/support/resolveWorkspace.js +16 -0
- package/src/server/support/workspaceActionSurfaces.js +118 -0
- package/src/server/support/workspaceInvitationsPolicy.js +45 -0
- package/src/server/support/workspaceRouteInput.js +22 -0
- package/src/server/workspaceBootstrapContributor.js +233 -0
- package/src/server/workspaceDirectory/bootWorkspaceDirectoryRoutes.js +133 -0
- package/src/server/workspaceDirectory/registerWorkspaceDirectory.js +19 -0
- package/src/server/workspaceDirectory/workspaceDirectoryActions.js +133 -0
- package/src/server/workspaceMembers/bootWorkspaceMembers.js +236 -0
- package/src/server/workspaceMembers/registerWorkspaceMembers.js +108 -0
- package/src/server/workspaceMembers/workspaceMembersActions.js +186 -0
- package/src/server/workspaceMembers/workspaceMembersService.js +222 -0
- package/src/server/workspacePendingInvitations/bootWorkspacePendingInvitations.js +62 -0
- package/src/server/workspacePendingInvitations/registerWorkspacePendingInvitations.js +119 -0
- package/src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js +74 -0
- package/src/server/workspacePendingInvitations/workspacePendingInvitationsService.js +138 -0
- package/src/server/workspaceSettings/bootWorkspaceSettings.js +76 -0
- package/src/server/workspaceSettings/registerWorkspaceSettings.js +62 -0
- package/src/server/workspaceSettings/workspaceSettingsActions.js +72 -0
- package/src/server/workspaceSettings/workspaceSettingsRepository.js +154 -0
- package/src/server/workspaceSettings/workspaceSettingsService.js +66 -0
- package/src/shared/operationMessages.js +16 -0
- package/src/shared/resources/resolveGlobalArrayRegistry.js +6 -0
- package/src/shared/resources/workspaceMembersResource.js +354 -0
- package/src/shared/resources/workspacePendingInvitationsResource.js +82 -0
- package/src/shared/resources/workspaceResource.js +176 -0
- package/src/shared/resources/workspaceSettingsFields.js +59 -0
- package/src/shared/resources/workspaceSettingsResource.js +169 -0
- package/src/shared/roles.js +161 -0
- package/src/shared/settings.js +119 -0
- package/src/shared/support/workspacePathModel.js +145 -0
- package/src/shared/tenancyMode.js +35 -0
- package/src/shared/tenancyProfile.js +73 -0
- package/templates/packages/main/src/shared/resources/workspaceSettingsFields.js +2 -2
- package/test/registerServiceRealtimeEvents.test.js +116 -0
- package/test/registerWorkspaceDirectory.test.js +31 -0
- package/test/registerWorkspaceSettings.test.js +40 -0
- package/test/repositoryContracts.test.js +34 -0
- package/test/resourcesCanonical.test.js +74 -0
- package/test/roles.test.js +159 -0
- package/test/routeParamsValidator.test.js +49 -0
- package/test/settingsFieldRegistriesSingleton.test.js +14 -0
- package/test/tenancyProfile.test.js +67 -0
- package/test/usersRouteResources.test.js +97 -0
- package/test/workspaceActionContextContributor.test.js +344 -0
- package/test/workspaceActionSurfaces.test.js +85 -0
- package/test/workspaceAuthPolicyContextResolver.test.js +119 -0
- package/test/workspaceBootstrapContributor.test.js +169 -0
- package/test/workspaceInvitationsPolicy.test.js +71 -0
- package/test/workspaceInvitesRepository.test.js +111 -0
- package/test/workspaceMembersService.test.js +398 -0
- package/test/workspacePathModel.test.js +93 -0
- package/test/workspacePendingInvitationsResource.test.js +38 -0
- package/test/workspacePendingInvitationsService.test.js +151 -0
- package/test/workspaceRouteVisibilityResolver.test.js +83 -0
- package/test/workspaceService.test.js +546 -0
- package/test/workspaceSettingsActions.test.js +52 -0
- package/test/workspaceSettingsRepository.test.js +202 -0
- package/test/workspaceSettingsResource.test.js +169 -0
- package/test/workspaceSettingsService.test.js +140 -0
- package/test/workspacesRouteRequestInputValidator.test.js +5 -5
- package/test-support/registerDefaultSettingsFields.js +1 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { Type } from "@fastify/type-provider-typebox";
|
|
2
|
+
import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/actions/textNormalization";
|
|
3
|
+
import {
|
|
4
|
+
normalizeObjectInput,
|
|
5
|
+
recordIdSchema,
|
|
6
|
+
recordIdInputSchema,
|
|
7
|
+
nullableRecordIdSchema
|
|
8
|
+
} from "@jskit-ai/kernel/shared/validators";
|
|
9
|
+
import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
|
|
10
|
+
import { createOperationMessages } from "../operationMessages.js";
|
|
11
|
+
import { createWorkspaceRoleCatalog, OWNER_ROLE_ID } from "../roles.js";
|
|
12
|
+
|
|
13
|
+
const workspaceSummaryOutputSchema = Type.Object(
|
|
14
|
+
{
|
|
15
|
+
id: recordIdSchema,
|
|
16
|
+
slug: Type.String({ minLength: 1 }),
|
|
17
|
+
name: Type.String({ minLength: 1 }),
|
|
18
|
+
ownerUserId: recordIdSchema,
|
|
19
|
+
avatarUrl: Type.String()
|
|
20
|
+
},
|
|
21
|
+
{ additionalProperties: false }
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const memberSummaryOutputSchema = Type.Object(
|
|
25
|
+
{
|
|
26
|
+
userId: recordIdSchema,
|
|
27
|
+
roleSid: Type.String({ minLength: 1 }),
|
|
28
|
+
status: Type.String({ minLength: 1 }),
|
|
29
|
+
displayName: Type.String(),
|
|
30
|
+
email: Type.String({ minLength: 1 }),
|
|
31
|
+
isOwner: Type.Boolean()
|
|
32
|
+
},
|
|
33
|
+
{ additionalProperties: false }
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const inviteSummaryOutputSchema = Type.Object(
|
|
37
|
+
{
|
|
38
|
+
id: recordIdSchema,
|
|
39
|
+
email: Type.String({ minLength: 3, format: "email" }),
|
|
40
|
+
roleSid: Type.String({ minLength: 1 }),
|
|
41
|
+
status: Type.String({ minLength: 1 }),
|
|
42
|
+
expiresAt: Type.String({ minLength: 1 }),
|
|
43
|
+
invitedByUserId: nullableRecordIdSchema
|
|
44
|
+
},
|
|
45
|
+
{ additionalProperties: false }
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
function normalizeWorkspaceAdminSummary(workspace) {
|
|
49
|
+
const source = normalizeObjectInput(workspace);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
id: normalizeRecordId(source.id, { fallback: "" }),
|
|
53
|
+
slug: normalizeText(source.slug),
|
|
54
|
+
name: normalizeText(source.name),
|
|
55
|
+
ownerUserId: normalizeRecordId(source.ownerUserId, { fallback: "" }),
|
|
56
|
+
avatarUrl: normalizeText(source.avatarUrl)
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function normalizeMemberSummary(member, workspace) {
|
|
61
|
+
const source = normalizeObjectInput(member);
|
|
62
|
+
const userId = normalizeRecordId(source.userId, { fallback: "" });
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
userId,
|
|
66
|
+
roleSid: normalizeLowerText(source.roleSid || "member") || "member",
|
|
67
|
+
status: normalizeLowerText(source.status || "active") || "active",
|
|
68
|
+
displayName: normalizeText(source.displayName),
|
|
69
|
+
email: normalizeLowerText(source.email),
|
|
70
|
+
isOwner: userId === workspace.ownerUserId || normalizeLowerText(source.roleSid) === OWNER_ROLE_ID
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normalizeInviteSummary(invite) {
|
|
75
|
+
const source = normalizeObjectInput(invite);
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
id: normalizeRecordId(source.id, { fallback: "" }),
|
|
79
|
+
email: normalizeLowerText(source.email),
|
|
80
|
+
roleSid: normalizeLowerText(source.roleSid || "member") || "member",
|
|
81
|
+
status: normalizeLowerText(source.status || "pending") || "pending",
|
|
82
|
+
expiresAt: source.expiresAt,
|
|
83
|
+
invitedByUserId: source.invitedByUserId == null ? null : normalizeRecordId(source.invitedByUserId, { fallback: null })
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function normalizeWorkspaceOutputEnvelope(
|
|
88
|
+
payload = {},
|
|
89
|
+
{ itemsKey, normalizeItem, includeInviteTokenPreview = false } = {}
|
|
90
|
+
) {
|
|
91
|
+
const source = normalizeObjectInput(payload);
|
|
92
|
+
const workspace = normalizeWorkspaceAdminSummary(source.workspace);
|
|
93
|
+
const items = Array.isArray(source[itemsKey]) ? source[itemsKey] : [];
|
|
94
|
+
const roleCatalog = normalizeObjectInput(source.roleCatalog);
|
|
95
|
+
const hasRoleCatalog =
|
|
96
|
+
Array.isArray(roleCatalog.roles) &&
|
|
97
|
+
roleCatalog.roles.length > 0 &&
|
|
98
|
+
Array.isArray(roleCatalog.assignableRoleIds);
|
|
99
|
+
const normalized = {
|
|
100
|
+
workspace,
|
|
101
|
+
[itemsKey]: items.map((item) => normalizeItem(item, workspace)),
|
|
102
|
+
roleCatalog: hasRoleCatalog ? roleCatalog : createWorkspaceRoleCatalog()
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
if (includeInviteTokenPreview && Object.hasOwn(source, "inviteTokenPreview")) {
|
|
106
|
+
normalized.inviteTokenPreview = normalizeText(source.inviteTokenPreview);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return normalized;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function normalizeWorkspaceMembersOutput(payload = {}) {
|
|
113
|
+
return normalizeWorkspaceOutputEnvelope(payload, {
|
|
114
|
+
itemsKey: "members",
|
|
115
|
+
normalizeItem: normalizeMemberSummary
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function normalizeWorkspaceInvitesOutput(payload = {}) {
|
|
120
|
+
return normalizeWorkspaceOutputEnvelope(payload, {
|
|
121
|
+
itemsKey: "invites",
|
|
122
|
+
normalizeItem: normalizeInviteSummary,
|
|
123
|
+
includeInviteTokenPreview: true
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const workspaceRoleCatalogOutputValidator = Object.freeze({
|
|
128
|
+
schema: Type.Object(
|
|
129
|
+
{
|
|
130
|
+
collaborationEnabled: Type.Boolean(),
|
|
131
|
+
defaultInviteRole: Type.String(),
|
|
132
|
+
roles: Type.Array(Type.Object({}, { additionalProperties: true })),
|
|
133
|
+
assignableRoleIds: Type.Array(Type.String({ minLength: 1 }))
|
|
134
|
+
},
|
|
135
|
+
{ additionalProperties: true }
|
|
136
|
+
)
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const workspaceMembersOutputValidator = Object.freeze({
|
|
140
|
+
schema: Type.Object(
|
|
141
|
+
{
|
|
142
|
+
workspace: workspaceSummaryOutputSchema,
|
|
143
|
+
members: Type.Array(memberSummaryOutputSchema),
|
|
144
|
+
roleCatalog: workspaceRoleCatalogOutputValidator.schema
|
|
145
|
+
},
|
|
146
|
+
{ additionalProperties: false }
|
|
147
|
+
),
|
|
148
|
+
normalize: normalizeWorkspaceMembersOutput
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const workspaceInvitesOutputValidator = Object.freeze({
|
|
152
|
+
schema: Type.Object(
|
|
153
|
+
{
|
|
154
|
+
workspace: workspaceSummaryOutputSchema,
|
|
155
|
+
invites: Type.Array(inviteSummaryOutputSchema),
|
|
156
|
+
roleCatalog: workspaceRoleCatalogOutputValidator.schema,
|
|
157
|
+
inviteTokenPreview: Type.Optional(Type.String({ minLength: 1 }))
|
|
158
|
+
},
|
|
159
|
+
{ additionalProperties: false }
|
|
160
|
+
),
|
|
161
|
+
normalize: normalizeWorkspaceInvitesOutput
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const updateMemberRoleBodyValidator = Object.freeze({
|
|
165
|
+
schema: Type.Object(
|
|
166
|
+
{
|
|
167
|
+
roleSid: Type.String({ minLength: 1 })
|
|
168
|
+
},
|
|
169
|
+
{ additionalProperties: false }
|
|
170
|
+
),
|
|
171
|
+
normalize(payload = {}) {
|
|
172
|
+
const source = normalizeObjectInput(payload);
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
roleSid: normalizeLowerText(source.roleSid)
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const updateMemberRoleInputValidator = Object.freeze({
|
|
181
|
+
schema: Type.Object(
|
|
182
|
+
{
|
|
183
|
+
memberUserId: recordIdInputSchema,
|
|
184
|
+
roleSid: Type.String({ minLength: 1 })
|
|
185
|
+
},
|
|
186
|
+
{ additionalProperties: false }
|
|
187
|
+
),
|
|
188
|
+
normalize(payload = {}) {
|
|
189
|
+
const source = normalizeObjectInput(payload);
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
memberUserId: normalizeRecordId(source.memberUserId, { fallback: "" }),
|
|
193
|
+
roleSid: normalizeLowerText(source.roleSid)
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const removeMemberInputValidator = Object.freeze({
|
|
199
|
+
schema: Type.Object(
|
|
200
|
+
{
|
|
201
|
+
memberUserId: recordIdInputSchema
|
|
202
|
+
},
|
|
203
|
+
{ additionalProperties: false }
|
|
204
|
+
),
|
|
205
|
+
normalize(payload = {}) {
|
|
206
|
+
const source = normalizeObjectInput(payload);
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
memberUserId: normalizeRecordId(source.memberUserId, { fallback: "" })
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const createInviteBodyValidator = Object.freeze({
|
|
215
|
+
schema: Type.Object(
|
|
216
|
+
{
|
|
217
|
+
email: Type.String({ minLength: 3, format: "email" }),
|
|
218
|
+
roleSid: Type.String({ minLength: 1 })
|
|
219
|
+
},
|
|
220
|
+
{ additionalProperties: false }
|
|
221
|
+
),
|
|
222
|
+
normalize(payload = {}) {
|
|
223
|
+
const source = normalizeObjectInput(payload);
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
email: normalizeLowerText(source.email),
|
|
227
|
+
roleSid: normalizeLowerText(source.roleSid || "member") || "member"
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const revokeInviteInputValidator = Object.freeze({
|
|
233
|
+
schema: Type.Object(
|
|
234
|
+
{
|
|
235
|
+
inviteId: recordIdInputSchema
|
|
236
|
+
},
|
|
237
|
+
{ additionalProperties: false }
|
|
238
|
+
),
|
|
239
|
+
normalize(payload = {}) {
|
|
240
|
+
const source = normalizeObjectInput(payload);
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
inviteId: normalizeRecordId(source.inviteId, { fallback: "" })
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const redeemInviteBodyValidator = Object.freeze({
|
|
249
|
+
schema: Type.Object(
|
|
250
|
+
{
|
|
251
|
+
token: Type.String({
|
|
252
|
+
minLength: 1,
|
|
253
|
+
messages: {
|
|
254
|
+
required: "Invite token is required.",
|
|
255
|
+
minLength: "Invite token is required.",
|
|
256
|
+
default: "Invite token is invalid."
|
|
257
|
+
}
|
|
258
|
+
}),
|
|
259
|
+
decision: Type.Union([Type.Literal("accept"), Type.Literal("refuse")], {
|
|
260
|
+
messages: {
|
|
261
|
+
required: "Decision is required.",
|
|
262
|
+
default: "Decision must be accept or refuse."
|
|
263
|
+
}
|
|
264
|
+
})
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
additionalProperties: false,
|
|
268
|
+
messages: {
|
|
269
|
+
additionalProperties: "Unexpected field."
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
),
|
|
273
|
+
normalize(payload = {}) {
|
|
274
|
+
const source = normalizeObjectInput(payload);
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
token: normalizeText(source.token),
|
|
278
|
+
decision: normalizeLowerText(source.decision)
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
const redeemInviteOutputValidator = Object.freeze({
|
|
284
|
+
schema: Type.Object(
|
|
285
|
+
{
|
|
286
|
+
decision: Type.Union([Type.Literal("accepted"), Type.Literal("refused")])
|
|
287
|
+
},
|
|
288
|
+
{ additionalProperties: false }
|
|
289
|
+
),
|
|
290
|
+
normalize(payload = {}) {
|
|
291
|
+
const source = normalizeObjectInput(payload);
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
decision: normalizeLowerText(source.decision)
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
const WORKSPACE_MEMBERS_MESSAGES = createOperationMessages();
|
|
300
|
+
|
|
301
|
+
const workspaceMembersResource = Object.freeze({
|
|
302
|
+
resource: "workspaceMembers",
|
|
303
|
+
messages: WORKSPACE_MEMBERS_MESSAGES,
|
|
304
|
+
operations: Object.freeze({
|
|
305
|
+
rolesList: Object.freeze({
|
|
306
|
+
method: "GET",
|
|
307
|
+
messages: WORKSPACE_MEMBERS_MESSAGES,
|
|
308
|
+
outputValidator: workspaceRoleCatalogOutputValidator
|
|
309
|
+
}),
|
|
310
|
+
membersList: Object.freeze({
|
|
311
|
+
method: "GET",
|
|
312
|
+
messages: WORKSPACE_MEMBERS_MESSAGES,
|
|
313
|
+
outputValidator: workspaceMembersOutputValidator
|
|
314
|
+
}),
|
|
315
|
+
updateMemberRole: Object.freeze({
|
|
316
|
+
method: "PATCH",
|
|
317
|
+
messages: WORKSPACE_MEMBERS_MESSAGES,
|
|
318
|
+
bodyValidator: updateMemberRoleBodyValidator,
|
|
319
|
+
inputValidator: updateMemberRoleInputValidator,
|
|
320
|
+
outputValidator: workspaceMembersOutputValidator
|
|
321
|
+
}),
|
|
322
|
+
removeMember: Object.freeze({
|
|
323
|
+
method: "DELETE",
|
|
324
|
+
messages: WORKSPACE_MEMBERS_MESSAGES,
|
|
325
|
+
inputValidator: removeMemberInputValidator,
|
|
326
|
+
outputValidator: workspaceMembersOutputValidator
|
|
327
|
+
}),
|
|
328
|
+
invitesList: Object.freeze({
|
|
329
|
+
method: "GET",
|
|
330
|
+
messages: WORKSPACE_MEMBERS_MESSAGES,
|
|
331
|
+
outputValidator: workspaceInvitesOutputValidator
|
|
332
|
+
}),
|
|
333
|
+
createInvite: Object.freeze({
|
|
334
|
+
method: "POST",
|
|
335
|
+
messages: WORKSPACE_MEMBERS_MESSAGES,
|
|
336
|
+
bodyValidator: createInviteBodyValidator,
|
|
337
|
+
outputValidator: workspaceInvitesOutputValidator
|
|
338
|
+
}),
|
|
339
|
+
revokeInvite: Object.freeze({
|
|
340
|
+
method: "DELETE",
|
|
341
|
+
messages: WORKSPACE_MEMBERS_MESSAGES,
|
|
342
|
+
inputValidator: revokeInviteInputValidator,
|
|
343
|
+
outputValidator: workspaceInvitesOutputValidator
|
|
344
|
+
}),
|
|
345
|
+
redeemInvite: Object.freeze({
|
|
346
|
+
method: "POST",
|
|
347
|
+
messages: WORKSPACE_MEMBERS_MESSAGES,
|
|
348
|
+
bodyValidator: redeemInviteBodyValidator,
|
|
349
|
+
outputValidator: redeemInviteOutputValidator
|
|
350
|
+
})
|
|
351
|
+
})
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
export { workspaceMembersResource };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Type } from "@fastify/type-provider-typebox";
|
|
2
|
+
import { encodeInviteTokenHash } from "@jskit-ai/auth-core/shared/inviteTokens";
|
|
3
|
+
import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/actions/textNormalization";
|
|
4
|
+
import { createOperationMessages } from "../operationMessages.js";
|
|
5
|
+
import { normalizeObjectInput, recordIdSchema } from "@jskit-ai/kernel/shared/validators";
|
|
6
|
+
import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
|
|
7
|
+
|
|
8
|
+
function normalizePendingInvite(invite) {
|
|
9
|
+
const id = normalizeRecordId(invite?.id, { fallback: null });
|
|
10
|
+
const workspaceId = normalizeRecordId(invite?.workspaceId, { fallback: null });
|
|
11
|
+
const tokenHash = normalizeText(invite?.tokenHash);
|
|
12
|
+
|
|
13
|
+
if (!id || !workspaceId || !tokenHash) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
id,
|
|
19
|
+
workspaceId,
|
|
20
|
+
workspaceSlug: normalizeText(invite?.workspaceSlug),
|
|
21
|
+
workspaceName: normalizeText(invite?.workspaceName || invite?.workspaceSlug),
|
|
22
|
+
workspaceAvatarUrl: normalizeText(invite?.workspaceAvatarUrl),
|
|
23
|
+
roleSid: normalizeLowerText(invite?.roleSid || "member") || "member",
|
|
24
|
+
status: normalizeLowerText(invite?.status || "pending") || "pending",
|
|
25
|
+
expiresAt: invite?.expiresAt || null,
|
|
26
|
+
token: encodeInviteTokenHash(tokenHash)
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizePendingInviteList(invites) {
|
|
31
|
+
return (Array.isArray(invites) ? invites : []).map((invite) => normalizePendingInvite(invite)).filter(Boolean);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const pendingInviteRecordValidator = Object.freeze({
|
|
35
|
+
schema: Type.Object(
|
|
36
|
+
{
|
|
37
|
+
id: recordIdSchema,
|
|
38
|
+
workspaceId: recordIdSchema,
|
|
39
|
+
workspaceSlug: Type.String({ minLength: 1 }),
|
|
40
|
+
workspaceName: Type.String({ minLength: 1 }),
|
|
41
|
+
workspaceAvatarUrl: Type.String(),
|
|
42
|
+
roleSid: Type.String({ minLength: 1 }),
|
|
43
|
+
status: Type.String({ minLength: 1 }),
|
|
44
|
+
expiresAt: Type.Union([Type.String({ minLength: 1 }), Type.Null()]),
|
|
45
|
+
token: Type.String({ minLength: 1 })
|
|
46
|
+
},
|
|
47
|
+
{ additionalProperties: false }
|
|
48
|
+
),
|
|
49
|
+
normalize: normalizePendingInvite
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const pendingInvitationsListOutputValidator = Object.freeze({
|
|
53
|
+
schema: Type.Object(
|
|
54
|
+
{
|
|
55
|
+
pendingInvites: Type.Array(pendingInviteRecordValidator.schema)
|
|
56
|
+
},
|
|
57
|
+
{ additionalProperties: false }
|
|
58
|
+
),
|
|
59
|
+
normalize(payload = {}) {
|
|
60
|
+
const source = normalizeObjectInput(payload);
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
pendingInvites: normalizePendingInviteList(source.pendingInvites)
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const WORKSPACE_PENDING_INVITATIONS_MESSAGES = createOperationMessages();
|
|
69
|
+
|
|
70
|
+
const workspacePendingInvitationsResource = Object.freeze({
|
|
71
|
+
resource: "workspacePendingInvitations",
|
|
72
|
+
messages: WORKSPACE_PENDING_INVITATIONS_MESSAGES,
|
|
73
|
+
operations: Object.freeze({
|
|
74
|
+
list: Object.freeze({
|
|
75
|
+
method: "GET",
|
|
76
|
+
messages: WORKSPACE_PENDING_INVITATIONS_MESSAGES,
|
|
77
|
+
outputValidator: pendingInvitationsListOutputValidator
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
export { workspacePendingInvitationsResource };
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/actions/textNormalization";
|
|
3
|
+
import {
|
|
4
|
+
normalizeObjectInput,
|
|
5
|
+
createCursorListValidator,
|
|
6
|
+
recordIdSchema,
|
|
7
|
+
recordIdInputSchema
|
|
8
|
+
} from "@jskit-ai/kernel/shared/validators";
|
|
9
|
+
import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
|
|
10
|
+
|
|
11
|
+
function normalizeWorkspaceAvatarUrl(value) {
|
|
12
|
+
const avatarUrl = normalizeText(value);
|
|
13
|
+
if (!avatarUrl) {
|
|
14
|
+
return "";
|
|
15
|
+
}
|
|
16
|
+
if (!avatarUrl.startsWith("http://") && !avatarUrl.startsWith("https://")) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
return new URL(avatarUrl).toString();
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalizeWorkspaceInput(payload = {}) {
|
|
27
|
+
const source = normalizeObjectInput(payload);
|
|
28
|
+
const normalized = {};
|
|
29
|
+
|
|
30
|
+
if (Object.hasOwn(source, "slug")) {
|
|
31
|
+
normalized.slug = normalizeLowerText(source.slug);
|
|
32
|
+
}
|
|
33
|
+
if (Object.hasOwn(source, "name")) {
|
|
34
|
+
normalized.name = normalizeText(source.name);
|
|
35
|
+
}
|
|
36
|
+
if (Object.hasOwn(source, "ownerUserId")) {
|
|
37
|
+
normalized.ownerUserId = normalizeRecordId(source.ownerUserId, { fallback: "" });
|
|
38
|
+
}
|
|
39
|
+
if (Object.hasOwn(source, "avatarUrl")) {
|
|
40
|
+
normalized.avatarUrl = normalizeWorkspaceAvatarUrl(source.avatarUrl);
|
|
41
|
+
}
|
|
42
|
+
if (Object.hasOwn(source, "isPersonal")) {
|
|
43
|
+
normalized.isPersonal = source.isPersonal === true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return normalized;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizeWorkspaceOutput(payload = {}) {
|
|
50
|
+
const source = normalizeObjectInput(payload);
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
id: normalizeRecordId(source.id, { fallback: "" }),
|
|
54
|
+
slug: normalizeLowerText(source.slug),
|
|
55
|
+
name: normalizeText(source.name),
|
|
56
|
+
ownerUserId: normalizeRecordId(source.ownerUserId, { fallback: "" }),
|
|
57
|
+
avatarUrl: normalizeText(source.avatarUrl)
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizeWorkspaceListItemOutput(payload = {}) {
|
|
62
|
+
const source = normalizeObjectInput(payload);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
id: normalizeRecordId(source.id, { fallback: "" }),
|
|
66
|
+
slug: normalizeLowerText(source.slug),
|
|
67
|
+
name: normalizeText(source.name),
|
|
68
|
+
avatarUrl: normalizeText(source.avatarUrl),
|
|
69
|
+
roleSid: normalizeLowerText(source.roleSid || "member") || "member",
|
|
70
|
+
isAccessible: source.isAccessible !== false
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const responseRecordSchema = Type.Object(
|
|
75
|
+
{
|
|
76
|
+
id: recordIdSchema,
|
|
77
|
+
slug: Type.String({ minLength: 1 }),
|
|
78
|
+
name: Type.String({ minLength: 1, maxLength: 160 }),
|
|
79
|
+
ownerUserId: recordIdSchema,
|
|
80
|
+
avatarUrl: Type.String()
|
|
81
|
+
},
|
|
82
|
+
{ additionalProperties: false }
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const listItemSchema = Type.Object(
|
|
86
|
+
{
|
|
87
|
+
id: recordIdSchema,
|
|
88
|
+
slug: Type.String({ minLength: 1 }),
|
|
89
|
+
name: Type.String({ minLength: 1, maxLength: 160 }),
|
|
90
|
+
avatarUrl: Type.String(),
|
|
91
|
+
roleSid: Type.String({ minLength: 1 }),
|
|
92
|
+
isAccessible: Type.Boolean()
|
|
93
|
+
},
|
|
94
|
+
{ additionalProperties: false }
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const createRequestBodySchema = Type.Object(
|
|
98
|
+
{
|
|
99
|
+
name: Type.String({ minLength: 1, maxLength: 160 }),
|
|
100
|
+
slug: Type.Optional(Type.String({ minLength: 1, maxLength: 120 })),
|
|
101
|
+
ownerUserId: Type.Optional(recordIdInputSchema)
|
|
102
|
+
},
|
|
103
|
+
{ additionalProperties: false }
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const patchRequestBodySchema = Type.Object(
|
|
107
|
+
{
|
|
108
|
+
name: Type.Optional(Type.String({ minLength: 1, maxLength: 160 })),
|
|
109
|
+
avatarUrl: Type.Optional(
|
|
110
|
+
Type.String({
|
|
111
|
+
pattern: "^(https?://.+)?$",
|
|
112
|
+
messages: {
|
|
113
|
+
pattern: "Workspace avatar URL must be a valid absolute URL (http:// or https://).",
|
|
114
|
+
default: "Workspace avatar URL must be a valid absolute URL (http:// or https://)."
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
)
|
|
118
|
+
},
|
|
119
|
+
{ additionalProperties: false }
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const responseRecordValidator = Object.freeze({
|
|
123
|
+
schema: responseRecordSchema,
|
|
124
|
+
normalize: normalizeWorkspaceOutput
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const workspaceSummaryOutputValidator = Object.freeze({
|
|
128
|
+
schema: listItemSchema,
|
|
129
|
+
normalize: normalizeWorkspaceListItemOutput
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const resource = {
|
|
133
|
+
resource: "workspace",
|
|
134
|
+
messages: {
|
|
135
|
+
validation: "Fix invalid workspace values and try again.",
|
|
136
|
+
saveSuccess: "Workspace updated.",
|
|
137
|
+
saveError: "Unable to update workspace.",
|
|
138
|
+
apiValidation: "Validation failed."
|
|
139
|
+
},
|
|
140
|
+
operations: {
|
|
141
|
+
view: {
|
|
142
|
+
method: "GET",
|
|
143
|
+
outputValidator: responseRecordValidator
|
|
144
|
+
},
|
|
145
|
+
list: {
|
|
146
|
+
method: "GET",
|
|
147
|
+
outputValidator: createCursorListValidator(workspaceSummaryOutputValidator)
|
|
148
|
+
},
|
|
149
|
+
create: {
|
|
150
|
+
method: "POST",
|
|
151
|
+
bodyValidator: {
|
|
152
|
+
schema: createRequestBodySchema,
|
|
153
|
+
normalize: normalizeWorkspaceInput
|
|
154
|
+
},
|
|
155
|
+
outputValidator: responseRecordValidator
|
|
156
|
+
},
|
|
157
|
+
replace: {
|
|
158
|
+
method: "PUT",
|
|
159
|
+
bodyValidator: {
|
|
160
|
+
schema: createRequestBodySchema,
|
|
161
|
+
normalize: normalizeWorkspaceInput
|
|
162
|
+
},
|
|
163
|
+
outputValidator: responseRecordValidator
|
|
164
|
+
},
|
|
165
|
+
patch: {
|
|
166
|
+
method: "PATCH",
|
|
167
|
+
bodyValidator: {
|
|
168
|
+
schema: patchRequestBodySchema,
|
|
169
|
+
normalize: normalizeWorkspaceInput
|
|
170
|
+
},
|
|
171
|
+
outputValidator: responseRecordValidator
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
export { resource as workspaceResource };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { normalizeText } from "@jskit-ai/kernel/shared/actions/textNormalization";
|
|
2
|
+
import { resolveGlobalArrayRegistry } from "./resolveGlobalArrayRegistry.js";
|
|
3
|
+
|
|
4
|
+
const workspaceSettingsFields = resolveGlobalArrayRegistry("jskit.workspaces-core.workspaceSettingsFields");
|
|
5
|
+
|
|
6
|
+
function defineField(field = {}) {
|
|
7
|
+
const key = normalizeText(field.key);
|
|
8
|
+
if (!key) {
|
|
9
|
+
throw new TypeError("workspaceSettingsFields.defineField requires field.key.");
|
|
10
|
+
}
|
|
11
|
+
if (workspaceSettingsFields.some((entry) => entry.key === key)) {
|
|
12
|
+
throw new Error(`workspaceSettingsFields.defineField duplicate key: ${key}`);
|
|
13
|
+
}
|
|
14
|
+
if (!field.inputSchema || typeof field.inputSchema !== "object") {
|
|
15
|
+
throw new TypeError(`workspaceSettingsFields.defineField("${key}") requires inputSchema.`);
|
|
16
|
+
}
|
|
17
|
+
if (!field.outputSchema || typeof field.outputSchema !== "object") {
|
|
18
|
+
throw new TypeError(`workspaceSettingsFields.defineField("${key}") requires outputSchema.`);
|
|
19
|
+
}
|
|
20
|
+
const dbColumn = normalizeText(field.dbColumn);
|
|
21
|
+
if (!dbColumn) {
|
|
22
|
+
throw new TypeError(`workspaceSettingsFields.defineField("${key}") requires dbColumn.`);
|
|
23
|
+
}
|
|
24
|
+
if (typeof field.normalizeInput !== "function") {
|
|
25
|
+
throw new TypeError(`workspaceSettingsFields.defineField("${key}") requires normalizeInput.`);
|
|
26
|
+
}
|
|
27
|
+
if (typeof field.normalizeOutput !== "function") {
|
|
28
|
+
throw new TypeError(`workspaceSettingsFields.defineField("${key}") requires normalizeOutput.`);
|
|
29
|
+
}
|
|
30
|
+
if (typeof field.resolveDefault !== "function") {
|
|
31
|
+
throw new TypeError(`workspaceSettingsFields.defineField("${key}") requires resolveDefault.`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
workspaceSettingsFields.push({
|
|
35
|
+
key,
|
|
36
|
+
dbColumn,
|
|
37
|
+
required: field.required !== false,
|
|
38
|
+
inputSchema: field.inputSchema,
|
|
39
|
+
outputSchema: field.outputSchema,
|
|
40
|
+
normalizeInput: field.normalizeInput,
|
|
41
|
+
normalizeOutput: field.normalizeOutput,
|
|
42
|
+
resolveDefault: field.resolveDefault
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function resetWorkspaceSettingsFields() {
|
|
47
|
+
workspaceSettingsFields.splice(0, workspaceSettingsFields.length);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resolveWorkspaceSettingsFieldKeys() {
|
|
51
|
+
return workspaceSettingsFields.map((field) => field.key);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export {
|
|
55
|
+
defineField,
|
|
56
|
+
resetWorkspaceSettingsFields,
|
|
57
|
+
resolveWorkspaceSettingsFieldKeys,
|
|
58
|
+
workspaceSettingsFields
|
|
59
|
+
};
|