@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.
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
@@ -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
+ };