@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,6 +1,17 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { createService } from "../src/server/common/services/
|
|
3
|
+
import { buildWorkspaceName, createService } from "../src/server/common/services/workspaceDirectoryService.js";
|
|
4
|
+
import { createWorkspaceRoleCatalog } from "../src/shared/roles.js";
|
|
5
|
+
import { resolveTenancyProfile } from "../src/shared/tenancyProfile.js";
|
|
6
|
+
|
|
7
|
+
// The service receives what the workspaces feature resolves from its config.
|
|
8
|
+
function resolvedSettings(appConfig) {
|
|
9
|
+
return {
|
|
10
|
+
tenancyProfile: resolveTenancyProfile(appConfig),
|
|
11
|
+
roleCatalog: createWorkspaceRoleCatalog(appConfig),
|
|
12
|
+
personalWorkspaceName: appConfig.personalWorkspaceName ?? buildWorkspaceName
|
|
13
|
+
};
|
|
14
|
+
}
|
|
4
15
|
|
|
5
16
|
function createRoleCatalog() {
|
|
6
17
|
return {
|
|
@@ -30,8 +41,8 @@ function createWorkspaceServiceFixture({
|
|
|
30
41
|
personalWorkspaceName = undefined,
|
|
31
42
|
personalWorkspace = {
|
|
32
43
|
id: "1",
|
|
33
|
-
slug: "
|
|
34
|
-
name: "
|
|
44
|
+
slug: "tony3",
|
|
45
|
+
name: "Tony3",
|
|
35
46
|
ownerUserId: "7",
|
|
36
47
|
isPersonal: true,
|
|
37
48
|
avatarUrl: ""
|
|
@@ -70,12 +81,12 @@ function createWorkspaceServiceFixture({
|
|
|
70
81
|
}
|
|
71
82
|
|
|
72
83
|
const service = createService({
|
|
73
|
-
|
|
84
|
+
...resolvedSettings({
|
|
74
85
|
tenancyMode,
|
|
75
86
|
tenancyPolicy,
|
|
76
87
|
personalWorkspaceName,
|
|
77
88
|
roleCatalog: roleCatalog && typeof roleCatalog === "object" ? { ...roleCatalog } : roleCatalog
|
|
78
|
-
},
|
|
89
|
+
}),
|
|
79
90
|
workspacesRepository: {
|
|
80
91
|
async findBySlug(slug) {
|
|
81
92
|
const normalizedSlug = String(slug || "").trim().toLowerCase();
|
|
@@ -97,8 +108,8 @@ function createWorkspaceServiceFixture({
|
|
|
97
108
|
return [
|
|
98
109
|
{
|
|
99
110
|
id: "1",
|
|
100
|
-
slug: "
|
|
101
|
-
name: "
|
|
111
|
+
slug: "tony3",
|
|
112
|
+
name: "Tony3",
|
|
102
113
|
avatarUrl: "",
|
|
103
114
|
roleSid: "owner",
|
|
104
115
|
membershipStatus: "active"
|
|
@@ -190,12 +201,12 @@ test("workspaceService.listWorkspacesForUser returns only accessible workspaces"
|
|
|
190
201
|
const { service, calls } = createWorkspaceServiceFixture();
|
|
191
202
|
const workspaces = await service.listWorkspacesForUser({
|
|
192
203
|
id: "7",
|
|
193
|
-
email: "
|
|
204
|
+
email: "chiara@example.com",
|
|
194
205
|
displayName: "Chiara"
|
|
195
206
|
});
|
|
196
207
|
|
|
197
208
|
assert.equal(workspaces.length, 1);
|
|
198
|
-
assert.equal(workspaces[0].slug, "
|
|
209
|
+
assert.equal(workspaces[0].slug, "tony3");
|
|
199
210
|
assert.equal(workspaces[0].roleSid, "owner");
|
|
200
211
|
assert.equal(calls.listForUserId, 1);
|
|
201
212
|
assert.equal(calls.insert, 0);
|
|
@@ -209,7 +220,7 @@ test("workspaceService.listWorkspacesForUser does not provision a personal works
|
|
|
209
220
|
|
|
210
221
|
await service.listWorkspacesForUser({
|
|
211
222
|
id: "7",
|
|
212
|
-
email: "
|
|
223
|
+
email: "chiara@example.com",
|
|
213
224
|
displayName: "Chiara"
|
|
214
225
|
});
|
|
215
226
|
|
|
@@ -223,7 +234,7 @@ test("workspaceService.listWorkspacesForUser returns all active memberships in p
|
|
|
223
234
|
userWorkspaceRows: [
|
|
224
235
|
{
|
|
225
236
|
id: "1",
|
|
226
|
-
slug: "
|
|
237
|
+
slug: "chiara",
|
|
227
238
|
name: "Chiara Personal",
|
|
228
239
|
avatarUrl: "",
|
|
229
240
|
roleSid: "owner",
|
|
@@ -231,7 +242,7 @@ test("workspaceService.listWorkspacesForUser returns all active memberships in p
|
|
|
231
242
|
},
|
|
232
243
|
{
|
|
233
244
|
id: "2",
|
|
234
|
-
slug: "
|
|
245
|
+
slug: "tony",
|
|
235
246
|
name: "Tony Workspace",
|
|
236
247
|
avatarUrl: "",
|
|
237
248
|
roleSid: "member",
|
|
@@ -250,13 +261,13 @@ test("workspaceService.listWorkspacesForUser returns all active memberships in p
|
|
|
250
261
|
|
|
251
262
|
const workspaces = await service.listWorkspacesForUser({
|
|
252
263
|
id: "7",
|
|
253
|
-
email: "
|
|
264
|
+
email: "chiara@example.com",
|
|
254
265
|
displayName: "Chiara"
|
|
255
266
|
});
|
|
256
267
|
|
|
257
268
|
assert.deepEqual(
|
|
258
269
|
workspaces.map((workspace) => workspace.slug),
|
|
259
|
-
["
|
|
270
|
+
["chiara", "tony"]
|
|
260
271
|
);
|
|
261
272
|
assert.equal(calls.findPersonalByOwnerUserId, 0);
|
|
262
273
|
assert.equal(calls.listForUserId, 1);
|
|
@@ -270,7 +281,7 @@ test("workspaceService.ensureProvisionedWorkspaceForAuthenticatedUser provisions
|
|
|
270
281
|
|
|
271
282
|
const workspace = await service.ensureProvisionedWorkspaceForAuthenticatedUser({
|
|
272
283
|
id: "7",
|
|
273
|
-
email: "
|
|
284
|
+
email: "chiara@example.com",
|
|
274
285
|
displayName: "Chiara"
|
|
275
286
|
});
|
|
276
287
|
|
|
@@ -297,7 +308,7 @@ test("workspaceService.ensureProvisionedWorkspaceForAuthenticatedUser provisions
|
|
|
297
308
|
|
|
298
309
|
const workspace = await service.ensureProvisionedWorkspaceForAuthenticatedUser({
|
|
299
310
|
id: "7",
|
|
300
|
-
email: "
|
|
311
|
+
email: "chiara@example.com",
|
|
301
312
|
displayName: "Chiara"
|
|
302
313
|
});
|
|
303
314
|
|
|
@@ -315,7 +326,7 @@ test("workspaceService.ensureProvisionedWorkspaceForAuthenticatedUser reuses the
|
|
|
315
326
|
|
|
316
327
|
const workspace = await service.ensureProvisionedWorkspaceForAuthenticatedUser({
|
|
317
328
|
id: "7",
|
|
318
|
-
email: "
|
|
329
|
+
email: "chiara@example.com",
|
|
319
330
|
displayName: "Chiara"
|
|
320
331
|
});
|
|
321
332
|
|
|
@@ -333,7 +344,7 @@ test("workspaceService.ensureProvisionedWorkspaceForAuthenticatedUser is a no-op
|
|
|
333
344
|
|
|
334
345
|
const result = await service.ensureProvisionedWorkspaceForAuthenticatedUser({
|
|
335
346
|
id: "7",
|
|
336
|
-
email: "
|
|
347
|
+
email: "chiara@example.com",
|
|
337
348
|
displayName: "Chiara"
|
|
338
349
|
});
|
|
339
350
|
|
|
@@ -351,17 +362,17 @@ test("workspaceService.createWorkspaceForAuthenticatedUser creates non-personal
|
|
|
351
362
|
}
|
|
352
363
|
});
|
|
353
364
|
|
|
354
|
-
const workspace = await service.createWorkspaceForAuthenticatedUser(
|
|
365
|
+
const workspace = (await service.createWorkspaceForAuthenticatedUser(
|
|
355
366
|
{
|
|
356
367
|
id: "7",
|
|
357
|
-
email: "
|
|
368
|
+
email: "chiara@example.com",
|
|
358
369
|
displayName: "Chiara"
|
|
359
370
|
},
|
|
360
371
|
{
|
|
361
372
|
name: "Operations Team",
|
|
362
373
|
slug: "ops-team"
|
|
363
374
|
}
|
|
364
|
-
);
|
|
375
|
+
)).value;
|
|
365
376
|
|
|
366
377
|
assert.equal(workspace.slug, "ops-team");
|
|
367
378
|
assert.equal(calls.insert, 1);
|
|
@@ -381,16 +392,16 @@ test("workspaceService.createWorkspaceForAuthenticatedUser generates an omitted
|
|
|
381
392
|
}
|
|
382
393
|
});
|
|
383
394
|
|
|
384
|
-
const workspace = await service.createWorkspaceForAuthenticatedUser(
|
|
395
|
+
const workspace = (await service.createWorkspaceForAuthenticatedUser(
|
|
385
396
|
{
|
|
386
397
|
id: "7",
|
|
387
|
-
email: "
|
|
398
|
+
email: "chiara@example.com",
|
|
388
399
|
displayName: "Chiara"
|
|
389
400
|
},
|
|
390
401
|
{
|
|
391
402
|
name: "Operations Team"
|
|
392
403
|
}
|
|
393
|
-
);
|
|
404
|
+
)).value;
|
|
394
405
|
|
|
395
406
|
assert.equal(workspace.slug, "operations-team");
|
|
396
407
|
assert.equal(calls.insert, 1);
|
|
@@ -411,7 +422,7 @@ test("workspaceService.createWorkspaceForAuthenticatedUser rejects creation when
|
|
|
411
422
|
service.createWorkspaceForAuthenticatedUser(
|
|
412
423
|
{
|
|
413
424
|
id: "7",
|
|
414
|
-
email: "
|
|
425
|
+
email: "chiara@example.com",
|
|
415
426
|
displayName: "Chiara"
|
|
416
427
|
},
|
|
417
428
|
{
|
|
@@ -433,10 +444,10 @@ test("workspaceService.resolveWorkspaceContextForUserBySlug returns workspace-no
|
|
|
433
444
|
service.resolveWorkspaceContextForUserBySlug(
|
|
434
445
|
{
|
|
435
446
|
id: "7",
|
|
436
|
-
email: "
|
|
447
|
+
email: "chiara@example.com",
|
|
437
448
|
displayName: "Chiara"
|
|
438
449
|
},
|
|
439
|
-
"
|
|
450
|
+
"tony3"
|
|
440
451
|
),
|
|
441
452
|
/Workspace not found/
|
|
442
453
|
);
|
|
@@ -468,7 +479,7 @@ test("workspaceService.resolveWorkspaceContextForUserBySlug allows personal tena
|
|
|
468
479
|
const context = await service.resolveWorkspaceContextForUserBySlug(
|
|
469
480
|
{
|
|
470
481
|
id: "7",
|
|
471
|
-
email: "
|
|
482
|
+
email: "chiara@example.com",
|
|
472
483
|
displayName: "Chiara"
|
|
473
484
|
},
|
|
474
485
|
"team-alpha"
|
|
@@ -484,19 +495,19 @@ test("workspaceService.resolveWorkspaceContextForUserBySlug grants owner access
|
|
|
484
495
|
let membershipRecord = null;
|
|
485
496
|
|
|
486
497
|
const service = createService({
|
|
487
|
-
|
|
498
|
+
...resolvedSettings({
|
|
488
499
|
tenancyMode: "personal",
|
|
489
500
|
roleCatalog: createRoleCatalog()
|
|
490
|
-
},
|
|
501
|
+
}),
|
|
491
502
|
workspacesRepository: {
|
|
492
503
|
async findBySlug(slug) {
|
|
493
|
-
if (String(slug) !== "
|
|
504
|
+
if (String(slug) !== "tony") {
|
|
494
505
|
return null;
|
|
495
506
|
}
|
|
496
507
|
return {
|
|
497
508
|
id: "1",
|
|
498
|
-
slug: "
|
|
499
|
-
name: "
|
|
509
|
+
slug: "tony",
|
|
510
|
+
name: "Tony",
|
|
500
511
|
ownerUserId: "7",
|
|
501
512
|
isPersonal: true,
|
|
502
513
|
avatarUrl: ""
|
|
@@ -539,10 +550,10 @@ test("workspaceService.resolveWorkspaceContextForUserBySlug grants owner access
|
|
|
539
550
|
const context = await service.resolveWorkspaceContextForUserBySlug(
|
|
540
551
|
{
|
|
541
552
|
id: "7",
|
|
542
|
-
email: "
|
|
553
|
+
email: "chiara@example.com",
|
|
543
554
|
displayName: "Chiara"
|
|
544
555
|
},
|
|
545
|
-
"
|
|
556
|
+
"tony"
|
|
546
557
|
);
|
|
547
558
|
|
|
548
559
|
assert.equal(ensuredMembershipCount, 1);
|
|
@@ -552,16 +563,16 @@ test("workspaceService.resolveWorkspaceContextForUserBySlug grants owner access
|
|
|
552
563
|
|
|
553
564
|
test("workspaceService.resolveWorkspaceContextForUserBySlug can resolve an existing workspace without membership", async () => {
|
|
554
565
|
const service = createService({
|
|
555
|
-
|
|
566
|
+
...resolvedSettings({
|
|
556
567
|
tenancyMode: "personal",
|
|
557
568
|
roleCatalog: createRoleCatalog()
|
|
558
|
-
},
|
|
569
|
+
}),
|
|
559
570
|
workspacesRepository: {
|
|
560
571
|
async findBySlug() {
|
|
561
572
|
return {
|
|
562
573
|
id: "42",
|
|
563
574
|
slug: "dog-and-groom",
|
|
564
|
-
name: "
|
|
575
|
+
name: "Harbor Grooming",
|
|
565
576
|
ownerUserId: "99",
|
|
566
577
|
isPersonal: false,
|
|
567
578
|
avatarUrl: ""
|
|
@@ -630,10 +641,10 @@ test("workspaceService.resolveWorkspaceContextForUserBySlug resolves permissions
|
|
|
630
641
|
const context = await service.resolveWorkspaceContextForUserBySlug(
|
|
631
642
|
{
|
|
632
643
|
id: "7",
|
|
633
|
-
email: "
|
|
644
|
+
email: "chiara@example.com",
|
|
634
645
|
displayName: "Chiara"
|
|
635
646
|
},
|
|
636
|
-
"
|
|
647
|
+
"tony3"
|
|
637
648
|
);
|
|
638
649
|
|
|
639
650
|
assert.deepEqual(context.permissions, ["workspace.settings.update"]);
|
|
@@ -653,14 +664,14 @@ test("workspaceService.getWorkspaceForAuthenticatedUser resolves workspace from
|
|
|
653
664
|
]
|
|
654
665
|
});
|
|
655
666
|
|
|
656
|
-
const workspace = await service.getWorkspaceForAuthenticatedUser(
|
|
667
|
+
const workspace = (await service.getWorkspaceForAuthenticatedUser(
|
|
657
668
|
{
|
|
658
669
|
id: "7",
|
|
659
|
-
email: "
|
|
670
|
+
email: "chiara@example.com",
|
|
660
671
|
displayName: "Chiara"
|
|
661
672
|
},
|
|
662
673
|
"team-alpha"
|
|
663
|
-
);
|
|
674
|
+
)).value;
|
|
664
675
|
|
|
665
676
|
assert.equal(workspace.slug, "team-alpha");
|
|
666
677
|
assert.equal(workspace.name, "Team Alpha");
|
|
@@ -669,18 +680,18 @@ test("workspaceService.getWorkspaceForAuthenticatedUser resolves workspace from
|
|
|
669
680
|
test("workspaceService.updateWorkspaceForAuthenticatedUser updates workspace profile fields", async () => {
|
|
670
681
|
const { service, calls } = createWorkspaceServiceFixture();
|
|
671
682
|
|
|
672
|
-
const workspace = await service.updateWorkspaceForAuthenticatedUser(
|
|
683
|
+
const workspace = (await service.updateWorkspaceForAuthenticatedUser(
|
|
673
684
|
{
|
|
674
685
|
id: "7",
|
|
675
|
-
email: "
|
|
686
|
+
email: "chiara@example.com",
|
|
676
687
|
displayName: "Chiara"
|
|
677
688
|
},
|
|
678
|
-
"
|
|
689
|
+
"tony3",
|
|
679
690
|
{
|
|
680
691
|
name: "Updated Workspace",
|
|
681
692
|
avatarUrl: "https://example.com/acme.png"
|
|
682
693
|
}
|
|
683
|
-
);
|
|
694
|
+
)).value;
|
|
684
695
|
|
|
685
696
|
assert.equal(calls.updateById, 1);
|
|
686
697
|
assert.equal(workspace.name, "Updated Workspace");
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
3
|
+
import { returnJsonApiData } from "@jskit-ai/http-runtime/shared";
|
|
4
|
+
import { createWorkspaceDirectoryActions } from "../src/server/workspaceDirectory/workspaceDirectoryActions.js";
|
|
5
|
+
import { createWorkspacePendingInvitationsActions } from "../src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js";
|
|
6
|
+
import { createWorkspaceMembersActions } from "../src/server/workspaceMembers/workspaceMembersActions.js";
|
|
7
|
+
import { createWorkspaceSettingsActions } from "../src/server/workspaceSettings/workspaceSettingsActions.js";
|
|
8
|
+
|
|
9
|
+
// The definitions only; no test here executes an action.
|
|
10
|
+
const workspaceDirectoryActionSpecifications = createWorkspaceDirectoryActions({ workspaceService: {} });
|
|
11
|
+
const workspacePendingInvitationsActionSpecifications = createWorkspacePendingInvitationsActions({ workspacePendingInvitationsService: {} });
|
|
12
|
+
const workspaceMembersActionSpecifications = createWorkspaceMembersActions({ workspaceMembersService: {} });
|
|
13
|
+
const workspaceSettingsActionSpecifications = createWorkspaceSettingsActions({ workspaceSettingsService: {} });
|
|
7
14
|
|
|
8
15
|
test("workspace settings action specifications stay explicit", () => {
|
|
9
16
|
assert.deepEqual(
|
|
@@ -64,7 +71,7 @@ test("workspace mutation actions declare their domain and bootstrap changes expl
|
|
|
64
71
|
);
|
|
65
72
|
const events = await Promise.all(settingsAction.events.map((builder) => builder({
|
|
66
73
|
input: { workspaceSlug: "acme", invitesEnabled: true },
|
|
67
|
-
result: {
|
|
74
|
+
result: returnJsonApiData({ invitesEnabled: true }),
|
|
68
75
|
context
|
|
69
76
|
})));
|
|
70
77
|
|
|
@@ -86,7 +93,7 @@ test("workspace invite actions carry recipient lookup as delivery policy, not se
|
|
|
86
93
|
};
|
|
87
94
|
const events = await Promise.all(action.events.map((builder) => builder({
|
|
88
95
|
input: { workspaceSlug: "acme", email: "reader@example.test", roleSid: "member" },
|
|
89
|
-
result: {
|
|
96
|
+
result: returnJsonApiData({ createdInviteId: 91 }),
|
|
90
97
|
context
|
|
91
98
|
})));
|
|
92
99
|
assert.equal(events[0].operation, "created");
|
|
@@ -116,7 +123,7 @@ test("accepting an invitation declares directory/member refresh while refusal do
|
|
|
116
123
|
async function emittedNames(decision) {
|
|
117
124
|
const built = await Promise.all(action.events.map((builder) => builder({
|
|
118
125
|
input: { decision, token: "opaque" },
|
|
119
|
-
result: {
|
|
126
|
+
result: returnJsonApiData({ workspaceId: 11 }),
|
|
120
127
|
context: { actor: { id: 7 } }
|
|
121
128
|
})));
|
|
122
129
|
return built.filter(Boolean).map((event) => event.realtime.event);
|
|
@@ -115,6 +115,8 @@ function createWorkspaceSettingsApiStub(rowOverrides = {}) {
|
|
|
115
115
|
test("workspaceSettingsRepository.findByWorkspaceId returns the canonical workspace-settings row", async () => {
|
|
116
116
|
const { api, DEFAULT_WORKSPACE_THEME, STUB_CREATED_AT } = createWorkspaceSettingsApiStub();
|
|
117
117
|
const repository = createRepository({
|
|
118
|
+
defaultInvitesEnabled: true,
|
|
119
|
+
defaultColorSettings: {},
|
|
118
120
|
api
|
|
119
121
|
});
|
|
120
122
|
|
|
@@ -139,6 +141,8 @@ test("workspaceSettingsRepository.findByWorkspaceId returns the canonical worksp
|
|
|
139
141
|
test("workspaceSettingsRepository.updateSettingsByWorkspaceId updates invitesEnabled only", async () => {
|
|
140
142
|
const { api, state } = createWorkspaceSettingsApiStub();
|
|
141
143
|
const repository = createRepository({
|
|
144
|
+
defaultInvitesEnabled: true,
|
|
145
|
+
defaultColorSettings: {},
|
|
142
146
|
api
|
|
143
147
|
});
|
|
144
148
|
|
|
@@ -154,6 +158,8 @@ test("workspaceSettingsRepository.ensureForWorkspaceId delegates defaults to the
|
|
|
154
158
|
const { api, state, DEFAULT_WORKSPACE_THEME } = createWorkspaceSettingsApiStub();
|
|
155
159
|
state.row = null;
|
|
156
160
|
const repository = createRepository({
|
|
161
|
+
defaultInvitesEnabled: true,
|
|
162
|
+
defaultColorSettings: {},
|
|
157
163
|
api
|
|
158
164
|
});
|
|
159
165
|
|
|
@@ -185,6 +191,7 @@ test("workspaceSettingsRepository.ensureForWorkspaceId starts new workspaces fro
|
|
|
185
191
|
const { api, state, DEFAULT_WORKSPACE_THEME } = createWorkspaceSettingsApiStub();
|
|
186
192
|
state.row = null;
|
|
187
193
|
const repository = createRepository({
|
|
194
|
+
defaultInvitesEnabled: true,
|
|
188
195
|
api,
|
|
189
196
|
defaultColorSettings: {
|
|
190
197
|
lightPrimaryColor: "#1C1C1C",
|
|
@@ -203,6 +210,8 @@ test("workspaceSettingsRepository.ensureForWorkspaceId starts new workspaces fro
|
|
|
203
210
|
test("workspaceSettingsRepository.updateSettingsByWorkspaceId updates workspace settings fields", async () => {
|
|
204
211
|
const { api, state } = createWorkspaceSettingsApiStub();
|
|
205
212
|
const repository = createRepository({
|
|
213
|
+
defaultInvitesEnabled: true,
|
|
214
|
+
defaultColorSettings: {},
|
|
206
215
|
api
|
|
207
216
|
});
|
|
208
217
|
|
|
@@ -218,6 +227,8 @@ test("workspaceSettingsRepository can be constructed without validating app conf
|
|
|
218
227
|
const { api } = createWorkspaceSettingsApiStub();
|
|
219
228
|
|
|
220
229
|
const repository = createRepository({
|
|
230
|
+
defaultInvitesEnabled: true,
|
|
231
|
+
defaultColorSettings: {},
|
|
221
232
|
api
|
|
222
233
|
});
|
|
223
234
|
|
|
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import { resolveWorkspaceThemePalettes } from "@jskit-ai/workspaces-core/shared/settings";
|
|
4
4
|
import { createService } from "../src/server/workspaceSettings/workspaceSettingsService.js";
|
|
5
|
+
import { createWorkspaceRoleCatalog } from "../src/shared/roles.js";
|
|
5
6
|
|
|
6
7
|
function authorizedOptions(permissions = []) {
|
|
7
8
|
return {
|
|
@@ -22,8 +23,8 @@ function createFixture({ workspaceInvitationsEnabled = true } = {}) {
|
|
|
22
23
|
settingsPatch: null,
|
|
23
24
|
workspace: {
|
|
24
25
|
id: 7,
|
|
25
|
-
slug: "
|
|
26
|
-
name: "
|
|
26
|
+
slug: "tony3",
|
|
27
|
+
name: "Tony3",
|
|
27
28
|
ownerUserId: 9
|
|
28
29
|
},
|
|
29
30
|
settings: {
|
|
@@ -55,7 +56,8 @@ function createFixture({ workspaceInvitationsEnabled = true } = {}) {
|
|
|
55
56
|
};
|
|
56
57
|
return state.settings;
|
|
57
58
|
}
|
|
58
|
-
}
|
|
59
|
+
},
|
|
60
|
+
roleCatalog: createWorkspaceRoleCatalog()
|
|
59
61
|
});
|
|
60
62
|
|
|
61
63
|
return { service, state };
|
|
@@ -64,10 +66,10 @@ function createFixture({ workspaceInvitationsEnabled = true } = {}) {
|
|
|
64
66
|
test("workspaceSettingsService.getWorkspaceSettings returns the stored invitesEnabled flag", async () => {
|
|
65
67
|
const { service, state } = createFixture();
|
|
66
68
|
|
|
67
|
-
const response = await service.getWorkspaceSettings(
|
|
69
|
+
const response = (await service.getWorkspaceSettings(
|
|
68
70
|
state.workspace,
|
|
69
71
|
authorizedOptions(["workspace.settings.view"])
|
|
70
|
-
);
|
|
72
|
+
)).value;
|
|
71
73
|
|
|
72
74
|
assert.deepEqual(response.settings, {
|
|
73
75
|
lightPrimaryColor: "#0F6B54",
|
|
@@ -87,13 +89,13 @@ test("workspaceSettingsService.getWorkspaceSettings returns the stored invitesEn
|
|
|
87
89
|
test("workspaceSettingsService.updateWorkspaceSettings writes editable fields through workspaceSettingsRepository only", async () => {
|
|
88
90
|
const { service, state } = createFixture();
|
|
89
91
|
|
|
90
|
-
const response = await service.updateWorkspaceSettings(
|
|
92
|
+
const response = (await service.updateWorkspaceSettings(
|
|
91
93
|
state.workspace,
|
|
92
94
|
{
|
|
93
95
|
invitesEnabled: false
|
|
94
96
|
},
|
|
95
97
|
authorizedOptions(["workspace.settings.update"])
|
|
96
|
-
);
|
|
98
|
+
)).value;
|
|
97
99
|
|
|
98
100
|
assert.deepEqual(state.settingsPatch, {
|
|
99
101
|
invitesEnabled: false
|
|
@@ -118,10 +120,10 @@ test("workspaceSettingsService disables invite settings in output when app polic
|
|
|
118
120
|
workspaceInvitationsEnabled: false
|
|
119
121
|
});
|
|
120
122
|
|
|
121
|
-
const response = await service.getWorkspaceSettings(
|
|
123
|
+
const response = (await service.getWorkspaceSettings(
|
|
122
124
|
state.workspace,
|
|
123
125
|
authorizedOptions(["workspace.settings.view"])
|
|
124
|
-
);
|
|
126
|
+
)).value;
|
|
125
127
|
|
|
126
128
|
assert.deepEqual(response.settings, {
|
|
127
129
|
lightPrimaryColor: "#0F6B54",
|
|
@@ -150,8 +150,8 @@ test("workspacesRepository.findById reads a canonical workspace row through json
|
|
|
150
150
|
rowsById: new Map([
|
|
151
151
|
["7", {
|
|
152
152
|
id: "7",
|
|
153
|
-
slug: "
|
|
154
|
-
name: "
|
|
153
|
+
slug: "tony3",
|
|
154
|
+
name: "Tony3",
|
|
155
155
|
ownerUserId: "9",
|
|
156
156
|
isPersonal: true,
|
|
157
157
|
avatarUrl: "",
|
|
@@ -167,8 +167,8 @@ test("workspacesRepository.findById reads a canonical workspace row through json
|
|
|
167
167
|
|
|
168
168
|
assert.deepEqual(workspace, {
|
|
169
169
|
id: "7",
|
|
170
|
-
slug: "
|
|
171
|
-
name: "
|
|
170
|
+
slug: "tony3",
|
|
171
|
+
name: "Tony3",
|
|
172
172
|
ownerUserId: "9",
|
|
173
173
|
isPersonal: true,
|
|
174
174
|
avatarUrl: "",
|
|
@@ -220,16 +220,16 @@ test("workspacesRepository.insert writes canonical fields through json-rest-api"
|
|
|
220
220
|
const repository = createRepository({ api });
|
|
221
221
|
|
|
222
222
|
const inserted = await repository.insert({
|
|
223
|
-
slug: "
|
|
224
|
-
name: "
|
|
223
|
+
slug: "Tony3",
|
|
224
|
+
name: "Tony3",
|
|
225
225
|
ownerUserId: "9",
|
|
226
226
|
avatarUrl: "",
|
|
227
227
|
isPersonal: false
|
|
228
228
|
});
|
|
229
229
|
|
|
230
230
|
assert.equal(state.postPayload.data.owner, "9");
|
|
231
|
-
assert.equal(state.postPayload.data.slug, "
|
|
232
|
-
assert.equal(state.postPayload.data.name, "
|
|
231
|
+
assert.equal(state.postPayload.data.slug, "Tony3");
|
|
232
|
+
assert.equal(state.postPayload.data.name, "Tony3");
|
|
233
233
|
assert.equal(state.postPayload.data.isPersonal, false);
|
|
234
234
|
assert.equal(state.postPayload.data.avatarUrl, "");
|
|
235
235
|
assert.equal(typeof state.postPayload.data.createdAt, "string");
|
|
@@ -271,8 +271,8 @@ test("workspacesRepository.updateById patches canonical fields and updatedAt", a
|
|
|
271
271
|
rowsById: new Map([
|
|
272
272
|
["7", {
|
|
273
273
|
id: "7",
|
|
274
|
-
slug: "
|
|
275
|
-
name: "
|
|
274
|
+
slug: "tony3",
|
|
275
|
+
name: "Tony3",
|
|
276
276
|
ownerUserId: "9",
|
|
277
277
|
isPersonal: false,
|
|
278
278
|
avatarUrl: "",
|
|
@@ -285,11 +285,11 @@ test("workspacesRepository.updateById patches canonical fields and updatedAt", a
|
|
|
285
285
|
const repository = createRepository({ api });
|
|
286
286
|
|
|
287
287
|
await repository.updateById("7", {
|
|
288
|
-
name: "
|
|
288
|
+
name: "Tony 4"
|
|
289
289
|
});
|
|
290
290
|
|
|
291
291
|
assert.equal(state.patchPayload.id, "7");
|
|
292
|
-
assert.equal(state.patchPayload.data.name, "
|
|
292
|
+
assert.equal(state.patchPayload.data.name, "Tony 4");
|
|
293
293
|
assert.equal(typeof state.patchPayload.data.updatedAt, "string");
|
|
294
294
|
});
|
|
295
295
|
|
|
@@ -302,8 +302,8 @@ test("workspacesRepository.listForUserId keeps membership fields outside the can
|
|
|
302
302
|
status: "active",
|
|
303
303
|
workspace: {
|
|
304
304
|
id: "7",
|
|
305
|
-
slug: "
|
|
306
|
-
name: "
|
|
305
|
+
slug: "tony3",
|
|
306
|
+
name: "Tony3",
|
|
307
307
|
ownerUserId: "9",
|
|
308
308
|
isPersonal: true,
|
|
309
309
|
avatarUrl: "",
|
|
@@ -353,8 +353,8 @@ test("workspacesRepository.listForUserId keeps membership fields outside the can
|
|
|
353
353
|
assert.deepEqual(workspaces, [
|
|
354
354
|
{
|
|
355
355
|
id: "7",
|
|
356
|
-
slug: "
|
|
357
|
-
name: "
|
|
356
|
+
slug: "tony3",
|
|
357
|
+
name: "Tony3",
|
|
358
358
|
ownerUserId: "9",
|
|
359
359
|
isPersonal: true,
|
|
360
360
|
avatarUrl: "",
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import { resolveTenancyProfile } from "../src/shared/tenancyProfile.js";
|
|
4
|
-
import { registerWorkspaceDirectoryRoutes } from "../src/server/workspaceDirectory/
|
|
5
|
-
import { registerWorkspaceMembersRoutes } from "../src/server/workspaceMembers/
|
|
6
|
-
import { registerWorkspacePendingInvitationsRoutes } from "../src/server/workspacePendingInvitations/
|
|
7
|
-
import { registerWorkspaceSettingsRoutes } from "../src/server/workspaceSettings/
|
|
4
|
+
import { registerWorkspaceDirectoryRoutes } from "../src/server/workspaceDirectory/workspaceDirectoryRoutes.js";
|
|
5
|
+
import { registerWorkspaceMembersRoutes } from "../src/server/workspaceMembers/workspaceMembersRoutes.js";
|
|
6
|
+
import { registerWorkspacePendingInvitationsRoutes } from "../src/server/workspacePendingInvitations/workspacePendingInvitationsRoutes.js";
|
|
7
|
+
import { registerWorkspaceSettingsRoutes } from "../src/server/workspaceSettings/workspaceSettingsRoutes.js";
|
|
8
8
|
|
|
9
9
|
function createReplyDouble() {
|
|
10
10
|
return {
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
normalizeDbRecordId,
|
|
3
|
-
retryDeadlocks,
|
|
4
|
-
toIsoString
|
|
5
|
-
} from "@jskit-ai/database-runtime/shared";
|
|
6
|
-
import { isDuplicateEntryError } from "@jskit-ai/database-runtime/shared/duplicateEntry";
|
|
7
|
-
import { normalizeLowerText, normalizeRecordId, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
8
|
-
|
|
9
|
-
function uniqueSorted(values) {
|
|
10
|
-
return [...new Set(values)].sort((left, right) => String(left).localeCompare(String(right)));
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function parseJson(value, fallback = {}) {
|
|
14
|
-
if (value == null) {
|
|
15
|
-
return fallback;
|
|
16
|
-
}
|
|
17
|
-
if (typeof value === "object") {
|
|
18
|
-
return value;
|
|
19
|
-
}
|
|
20
|
-
try {
|
|
21
|
-
return JSON.parse(String(value));
|
|
22
|
-
} catch {
|
|
23
|
-
return fallback;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function toDbJson(value, fallback = {}) {
|
|
28
|
-
const source = value && typeof value === "object" ? value : fallback;
|
|
29
|
-
return JSON.stringify(source);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export {
|
|
33
|
-
toIsoString,
|
|
34
|
-
retryDeadlocks,
|
|
35
|
-
isDuplicateEntryError,
|
|
36
|
-
normalizeText,
|
|
37
|
-
normalizeLowerText,
|
|
38
|
-
normalizeRecordId,
|
|
39
|
-
normalizeDbRecordId,
|
|
40
|
-
uniqueSorted,
|
|
41
|
-
parseJson,
|
|
42
|
-
toDbJson
|
|
43
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { deepFreeze } from "@jskit-ai/kernel/shared/support/deepFreeze";
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { normalizeObject } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
-
|
|
3
|
-
function resolveActionUser(context, input) {
|
|
4
|
-
const payload = normalizeObject(input);
|
|
5
|
-
const request = context?.requestMeta?.request || null;
|
|
6
|
-
return payload.user || request?.user || context?.actor || null;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export { resolveActionUser };
|