@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
|
@@ -7,11 +7,11 @@ import {
|
|
|
7
7
|
DEFAULT_WORKSPACE_DARK_PALETTE,
|
|
8
8
|
DEFAULT_WORKSPACE_LIGHT_PALETTE,
|
|
9
9
|
coerceWorkspaceThemeColor
|
|
10
|
-
} from "@jskit-ai/
|
|
10
|
+
} from "@jskit-ai/workspaces-core/shared/settings";
|
|
11
11
|
import {
|
|
12
12
|
defineField,
|
|
13
13
|
resetWorkspaceSettingsFields
|
|
14
|
-
} from "@jskit-ai/
|
|
14
|
+
} from "@jskit-ai/workspaces-core/shared/resources/workspaceSettingsFields";
|
|
15
15
|
|
|
16
16
|
function normalizeHexColor(value) {
|
|
17
17
|
const color = normalizeText(value);
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { registerWorkspaceMembers } from "../src/server/workspaceMembers/registerWorkspaceMembers.js";
|
|
4
|
+
import { registerWorkspacePendingInvitations } from "../src/server/workspacePendingInvitations/registerWorkspacePendingInvitations.js";
|
|
5
|
+
|
|
6
|
+
function createAppDouble() {
|
|
7
|
+
const serviceCalls = [];
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
serviceCalls,
|
|
11
|
+
app: {
|
|
12
|
+
singleton() {
|
|
13
|
+
return this;
|
|
14
|
+
},
|
|
15
|
+
service(token, factory, metadata) {
|
|
16
|
+
serviceCalls.push({
|
|
17
|
+
token,
|
|
18
|
+
factory,
|
|
19
|
+
metadata
|
|
20
|
+
});
|
|
21
|
+
return this;
|
|
22
|
+
},
|
|
23
|
+
actions() {
|
|
24
|
+
return this;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function findServiceCall(serviceCalls, token) {
|
|
31
|
+
return serviceCalls.find((entry) => entry.token === token) || null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
test("workspace register functions publish members/invites/workspace-list realtime events", async () => {
|
|
35
|
+
const membersApp = createAppDouble();
|
|
36
|
+
registerWorkspaceMembers(membersApp.app);
|
|
37
|
+
const members = findServiceCall(membersApp.serviceCalls, "workspaces.members.service");
|
|
38
|
+
assert.equal(members?.metadata?.events?.updateMemberRole?.[0]?.realtime?.event, "workspace.members.changed");
|
|
39
|
+
assert.equal(members?.metadata?.events?.updateMemberRole?.[1]?.realtime?.event, "users.bootstrap.changed");
|
|
40
|
+
assert.equal(members?.metadata?.events?.removeMember?.[0]?.realtime?.event, "workspace.members.changed");
|
|
41
|
+
assert.equal(members?.metadata?.events?.removeMember?.[1]?.realtime?.event, "users.bootstrap.changed");
|
|
42
|
+
assert.equal(members?.metadata?.events?.createInvite?.[0]?.realtime?.event, "workspace.invites.changed");
|
|
43
|
+
assert.equal(members?.metadata?.events?.createInvite?.[1]?.realtime?.event, "users.bootstrap.changed");
|
|
44
|
+
assert.equal(members?.metadata?.events?.createInvite?.[1]?.entityId?.({ result: { createdInviteId: "91" } }), "91");
|
|
45
|
+
assert.equal(members?.metadata?.events?.createInvite?.[1]?.realtime?.audience?.preset, "event_scope");
|
|
46
|
+
assert.equal(typeof members?.metadata?.events?.createInvite?.[1]?.realtime?.audience?.userQuery, "function");
|
|
47
|
+
const createInviteAudienceQueryResult = await members?.metadata?.events?.createInvite?.[1]?.realtime?.audience?.userQuery({
|
|
48
|
+
knex() {
|
|
49
|
+
return {
|
|
50
|
+
join() {
|
|
51
|
+
return this;
|
|
52
|
+
},
|
|
53
|
+
where(field, value) {
|
|
54
|
+
assert.equal(field, "wi.id");
|
|
55
|
+
assert.equal(value, "91");
|
|
56
|
+
return this;
|
|
57
|
+
},
|
|
58
|
+
async first() {
|
|
59
|
+
return {
|
|
60
|
+
user_id: 55
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
event: {
|
|
66
|
+
entityId: "91"
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
assert.deepEqual(createInviteAudienceQueryResult, [{ userId: "55" }]);
|
|
70
|
+
assert.equal(members?.metadata?.events?.revokeInvite?.[0]?.realtime?.event, "workspace.invites.changed");
|
|
71
|
+
assert.equal(members?.metadata?.events?.revokeInvite?.[1]?.realtime?.event, "users.bootstrap.changed");
|
|
72
|
+
assert.equal(members?.metadata?.events?.revokeInvite?.[1]?.entityId?.({ result: { revokedInviteId: "19" } }), "19");
|
|
73
|
+
assert.equal(members?.metadata?.events?.revokeInvite?.[1]?.realtime?.audience?.preset, "event_scope");
|
|
74
|
+
assert.equal(typeof members?.metadata?.events?.revokeInvite?.[1]?.realtime?.audience?.userQuery, "function");
|
|
75
|
+
|
|
76
|
+
const pendingApp = createAppDouble();
|
|
77
|
+
registerWorkspacePendingInvitations(pendingApp.app);
|
|
78
|
+
const pending = findServiceCall(pendingApp.serviceCalls, "workspaces.pending-invitations.service");
|
|
79
|
+
const acceptInviteEvents = Array.isArray(pending?.metadata?.events?.acceptInviteByToken)
|
|
80
|
+
? pending.metadata.events.acceptInviteByToken
|
|
81
|
+
: [];
|
|
82
|
+
const acceptInviteRealtimeEvents = acceptInviteEvents.map((entry) => entry?.realtime?.event).filter(Boolean);
|
|
83
|
+
assert.ok(acceptInviteRealtimeEvents.includes("workspace.invitations.pending.changed"));
|
|
84
|
+
assert.ok(acceptInviteRealtimeEvents.includes("users.bootstrap.changed"));
|
|
85
|
+
assert.ok(acceptInviteRealtimeEvents.includes("workspaces.changed"));
|
|
86
|
+
assert.ok(acceptInviteRealtimeEvents.includes("workspace.members.changed"));
|
|
87
|
+
assert.ok(acceptInviteRealtimeEvents.includes("workspace.invites.changed"));
|
|
88
|
+
|
|
89
|
+
const acceptedMembersChange = acceptInviteEvents.find(
|
|
90
|
+
(entry) => entry?.realtime?.event === "workspace.members.changed"
|
|
91
|
+
);
|
|
92
|
+
assert.equal(acceptedMembersChange?.entityId?.({ result: { workspaceId: "9" } }), "9");
|
|
93
|
+
assert.deepEqual(
|
|
94
|
+
acceptedMembersChange?.realtime?.audience?.({
|
|
95
|
+
event: {
|
|
96
|
+
entityId: "9"
|
|
97
|
+
}
|
|
98
|
+
}),
|
|
99
|
+
{
|
|
100
|
+
workspaceId: "9"
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const acceptedInvitesChange = acceptInviteEvents.find(
|
|
105
|
+
(entry) => entry?.realtime?.event === "workspace.invites.changed"
|
|
106
|
+
);
|
|
107
|
+
assert.equal(acceptedInvitesChange?.entityId?.({ result: { workspaceId: "9" } }), "9");
|
|
108
|
+
|
|
109
|
+
const refuseInviteEvents = Array.isArray(pending?.metadata?.events?.refuseInviteByToken)
|
|
110
|
+
? pending.metadata.events.refuseInviteByToken
|
|
111
|
+
: [];
|
|
112
|
+
const refuseInviteRealtimeEvents = refuseInviteEvents.map((entry) => entry?.realtime?.event).filter(Boolean);
|
|
113
|
+
assert.ok(refuseInviteRealtimeEvents.includes("workspace.invitations.pending.changed"));
|
|
114
|
+
assert.ok(refuseInviteRealtimeEvents.includes("users.bootstrap.changed"));
|
|
115
|
+
assert.ok(refuseInviteRealtimeEvents.includes("workspace.invites.changed"));
|
|
116
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { registerWorkspaceDirectory } from "../src/server/workspaceDirectory/registerWorkspaceDirectory.js";
|
|
4
|
+
|
|
5
|
+
function createAppDouble() {
|
|
6
|
+
const actionBatches = [];
|
|
7
|
+
|
|
8
|
+
return {
|
|
9
|
+
actionBatches,
|
|
10
|
+
singleton() {},
|
|
11
|
+
actions(entries) {
|
|
12
|
+
actionBatches.push(Array.isArray(entries) ? entries : [entries]);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function listActionIds(app) {
|
|
18
|
+
return app.actionBatches.flat().map((entry) => String(entry?.id || ""));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
test("registerWorkspaceDirectory registers workspace directory actions without resolving runtime tenancy tokens", () => {
|
|
22
|
+
const app = createAppDouble();
|
|
23
|
+
|
|
24
|
+
registerWorkspaceDirectory(app);
|
|
25
|
+
assert.deepEqual(listActionIds(app), [
|
|
26
|
+
"workspace.workspaces.create",
|
|
27
|
+
"workspace.workspaces.list",
|
|
28
|
+
"workspace.workspaces.read",
|
|
29
|
+
"workspace.workspaces.update"
|
|
30
|
+
]);
|
|
31
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { registerWorkspaceSettings } from "../src/server/workspaceSettings/registerWorkspaceSettings.js";
|
|
4
|
+
|
|
5
|
+
test("registerWorkspaceSettings registers workspace settings service realtime event metadata", () => {
|
|
6
|
+
const singletonBindings = new Map();
|
|
7
|
+
const actionCalls = [];
|
|
8
|
+
const serviceCalls = [];
|
|
9
|
+
|
|
10
|
+
const app = {
|
|
11
|
+
singleton(token, factory) {
|
|
12
|
+
singletonBindings.set(token, factory);
|
|
13
|
+
return this;
|
|
14
|
+
},
|
|
15
|
+
service(token, factory, metadata) {
|
|
16
|
+
serviceCalls.push({
|
|
17
|
+
token,
|
|
18
|
+
factory,
|
|
19
|
+
metadata
|
|
20
|
+
});
|
|
21
|
+
return this;
|
|
22
|
+
},
|
|
23
|
+
actions(definitions) {
|
|
24
|
+
actionCalls.push(definitions);
|
|
25
|
+
return this;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
registerWorkspaceSettings(app);
|
|
30
|
+
|
|
31
|
+
assert.equal(singletonBindings.has("workspaceSettingsRepository"), true);
|
|
32
|
+
assert.equal(serviceCalls.length, 1);
|
|
33
|
+
assert.equal(serviceCalls[0].token, "workspaces.settings.service");
|
|
34
|
+
assert.equal(typeof serviceCalls[0].factory, "function");
|
|
35
|
+
assert.equal(serviceCalls[0].metadata?.events?.updateWorkspaceSettings?.[0]?.realtime?.event, "workspace.settings.changed");
|
|
36
|
+
assert.equal(serviceCalls[0].metadata?.events?.updateWorkspaceSettings?.[0]?.realtime?.audience, "event_scope");
|
|
37
|
+
assert.equal(serviceCalls[0].metadata?.events?.updateWorkspaceSettings?.[1]?.realtime?.event, "users.bootstrap.changed");
|
|
38
|
+
assert.equal(serviceCalls[0].metadata?.events?.updateWorkspaceSettings?.[1]?.realtime?.audience, "event_scope");
|
|
39
|
+
assert.equal(actionCalls.length, 1);
|
|
40
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createRepository as createWorkspaceInvitesRepository } from "../src/server/common/repositories/workspaceInvitesRepository.js";
|
|
4
|
+
import { createRepository as createWorkspaceMembershipsRepository } from "../src/server/common/repositories/workspaceMembershipsRepository.js";
|
|
5
|
+
import { createRepository as createWorkspacesRepository } from "../src/server/common/repositories/workspacesRepository.js";
|
|
6
|
+
import { createRepository as createWorkspaceSettingsRepository } from "../src/server/workspaceSettings/workspaceSettingsRepository.js";
|
|
7
|
+
|
|
8
|
+
function createKnexStub() {
|
|
9
|
+
const knex = Object.assign(() => {
|
|
10
|
+
throw new Error("query execution not expected");
|
|
11
|
+
}, {
|
|
12
|
+
async transaction(work) {
|
|
13
|
+
return work({ trxId: "trx-1" });
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
return knex;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
test("workspaces-core repositories expose withTransaction", async () => {
|
|
21
|
+
const knex = createKnexStub();
|
|
22
|
+
const repositories = [
|
|
23
|
+
createWorkspaceInvitesRepository(knex),
|
|
24
|
+
createWorkspaceMembershipsRepository(knex),
|
|
25
|
+
createWorkspacesRepository(knex),
|
|
26
|
+
createWorkspaceSettingsRepository(knex)
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
for (const repository of repositories) {
|
|
30
|
+
assert.equal(typeof repository.withTransaction, "function");
|
|
31
|
+
const result = await repository.withTransaction(async (trx) => ({ id: trx.trxId }));
|
|
32
|
+
assert.deepEqual(result, { id: "trx-1" });
|
|
33
|
+
}
|
|
34
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import "../test-support/registerDefaultSettingsFields.js";
|
|
7
|
+
import { workspaceMembersResource } from "../src/shared/resources/workspaceMembersResource.js";
|
|
8
|
+
import { workspaceResource } from "../src/shared/resources/workspaceResource.js";
|
|
9
|
+
import { workspaceSettingsResource } from "../src/shared/resources/workspaceSettingsResource.js";
|
|
10
|
+
|
|
11
|
+
function assertResourceOperationMessages(resource, operationName, label) {
|
|
12
|
+
const operation = resource?.operations?.[operationName];
|
|
13
|
+
assert.equal(typeof operation, "object", `${label}.operations.${operationName} must exist.`);
|
|
14
|
+
|
|
15
|
+
const operationMessages = operation?.messages;
|
|
16
|
+
const resourceMessages = resource?.messages || resource?.operationMessages;
|
|
17
|
+
const resolvedMessages =
|
|
18
|
+
operationMessages && typeof operationMessages === "object"
|
|
19
|
+
? operationMessages
|
|
20
|
+
: resourceMessages;
|
|
21
|
+
|
|
22
|
+
assert.equal(
|
|
23
|
+
typeof resolvedMessages,
|
|
24
|
+
"object",
|
|
25
|
+
`${label}.operations.${operationName} must resolve operation messages from operation.messages or resource.messages.`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
test("workspaces-core resources expose messages for all operations", () => {
|
|
30
|
+
const resources = {
|
|
31
|
+
workspace: workspaceResource,
|
|
32
|
+
workspaceSettings: workspaceSettingsResource
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
for (const [label, resource] of Object.entries(resources)) {
|
|
36
|
+
for (const operationName of ["view", "list", "create", "replace", "patch"]) {
|
|
37
|
+
assertResourceOperationMessages(resource, operationName, label);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("workspaces-core specialized resource operations expose messages and validators", () => {
|
|
43
|
+
const operationSpecs = [
|
|
44
|
+
{ label: "workspaceMembers.rolesList", operation: workspaceMembersResource.operations.rolesList },
|
|
45
|
+
{ label: "workspaceMembers.membersList", operation: workspaceMembersResource.operations.membersList },
|
|
46
|
+
{ label: "workspaceMembers.updateMemberRole", operation: workspaceMembersResource.operations.updateMemberRole },
|
|
47
|
+
{ label: "workspaceMembers.removeMember", operation: workspaceMembersResource.operations.removeMember },
|
|
48
|
+
{ label: "workspaceMembers.invitesList", operation: workspaceMembersResource.operations.invitesList },
|
|
49
|
+
{ label: "workspaceMembers.createInvite", operation: workspaceMembersResource.operations.createInvite },
|
|
50
|
+
{ label: "workspaceMembers.revokeInvite", operation: workspaceMembersResource.operations.revokeInvite },
|
|
51
|
+
{ label: "workspaceMembers.redeemInvite", operation: workspaceMembersResource.operations.redeemInvite }
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
for (const { label, operation } of operationSpecs) {
|
|
55
|
+
assert.equal(typeof operation?.messages, "object", `${label}.messages must be an object.`);
|
|
56
|
+
assert.equal(typeof operation?.outputValidator?.schema, "object", `${label}.outputValidator.schema must exist.`);
|
|
57
|
+
if (operation?.bodyValidator) {
|
|
58
|
+
assert.equal(typeof operation.bodyValidator.schema, "object", `${label}.bodyValidator.schema must exist.`);
|
|
59
|
+
}
|
|
60
|
+
if (operation?.paramsValidator) {
|
|
61
|
+
assert.equal(typeof operation.paramsValidator.schema, "object", `${label}.paramsValidator.schema must exist.`);
|
|
62
|
+
}
|
|
63
|
+
if (operation?.queryValidator) {
|
|
64
|
+
assert.equal(typeof operation.queryValidator.schema, "object", `${label}.queryValidator.schema must exist.`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("workspaces-core no longer contains legacy shared/schema directory", () => {
|
|
70
|
+
const testFilePath = fileURLToPath(import.meta.url);
|
|
71
|
+
const packageRoot = path.resolve(path.dirname(testFilePath), "..");
|
|
72
|
+
const legacySchemaDir = path.join(packageRoot, "src", "shared", "schema");
|
|
73
|
+
assert.equal(existsSync(legacySchemaDir), false, "src/shared/schema must not exist.");
|
|
74
|
+
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
createWorkspaceRoleCatalog,
|
|
5
|
+
cloneWorkspaceRoleCatalog,
|
|
6
|
+
resolveRolePermissions,
|
|
7
|
+
hasPermission
|
|
8
|
+
} from "../src/shared/roles.js";
|
|
9
|
+
|
|
10
|
+
test("createWorkspaceRoleCatalog resolves role descriptors only from appConfig.roleCatalog", () => {
|
|
11
|
+
const emptyCatalog = createWorkspaceRoleCatalog();
|
|
12
|
+
assert.deepEqual(emptyCatalog.roles, []);
|
|
13
|
+
assert.deepEqual(emptyCatalog.assignableRoleIds, []);
|
|
14
|
+
assert.equal(emptyCatalog.defaultInviteRole, "");
|
|
15
|
+
assert.equal(emptyCatalog.collaborationEnabled, false);
|
|
16
|
+
|
|
17
|
+
const appConfig = {
|
|
18
|
+
roleCatalog: {
|
|
19
|
+
workspace: {
|
|
20
|
+
defaultInviteRole: "editor"
|
|
21
|
+
},
|
|
22
|
+
roles: {
|
|
23
|
+
owner: {
|
|
24
|
+
assignable: false,
|
|
25
|
+
permissions: ["workspace.settings.update"]
|
|
26
|
+
},
|
|
27
|
+
editor: {
|
|
28
|
+
assignable: true,
|
|
29
|
+
permissions: ["crud.contacts.*"]
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
const roleCatalog = createWorkspaceRoleCatalog(appConfig);
|
|
35
|
+
const editorRole = roleCatalog.roles.find((role) => role.id === "editor");
|
|
36
|
+
|
|
37
|
+
assert.equal(roleCatalog.defaultInviteRole, "editor");
|
|
38
|
+
assert.equal(roleCatalog.assignableRoleIds.includes("editor"), true);
|
|
39
|
+
assert.deepEqual(resolveRolePermissions("owner", appConfig), ["workspace.settings.update"]);
|
|
40
|
+
assert.equal(hasPermission(editorRole?.permissions, "crud.contacts.update"), true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("createWorkspaceRoleCatalog resolves inherited role permissions with parent permissions first", () => {
|
|
44
|
+
const appConfig = {
|
|
45
|
+
roleCatalog: {
|
|
46
|
+
workspace: {
|
|
47
|
+
defaultInviteRole: "member"
|
|
48
|
+
},
|
|
49
|
+
roles: {
|
|
50
|
+
member: {
|
|
51
|
+
assignable: true,
|
|
52
|
+
permissions: [
|
|
53
|
+
"workspace.settings.view",
|
|
54
|
+
"crud.contacts.list"
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
admin: {
|
|
58
|
+
assignable: true,
|
|
59
|
+
inherits: "member",
|
|
60
|
+
permissions: [
|
|
61
|
+
"workspace.settings.update",
|
|
62
|
+
"workspace.members.manage",
|
|
63
|
+
"workspace.settings.view"
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const roleCatalog = createWorkspaceRoleCatalog(appConfig);
|
|
71
|
+
const adminRole = roleCatalog.roles.find((role) => role.id === "admin");
|
|
72
|
+
|
|
73
|
+
assert.deepEqual(adminRole, {
|
|
74
|
+
id: "admin",
|
|
75
|
+
assignable: true,
|
|
76
|
+
permissions: [
|
|
77
|
+
"workspace.settings.view",
|
|
78
|
+
"crud.contacts.list",
|
|
79
|
+
"workspace.settings.update",
|
|
80
|
+
"workspace.members.manage"
|
|
81
|
+
]
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("createWorkspaceRoleCatalog rejects unknown inherited roles", () => {
|
|
86
|
+
assert.throws(
|
|
87
|
+
() =>
|
|
88
|
+
createWorkspaceRoleCatalog({
|
|
89
|
+
roleCatalog: {
|
|
90
|
+
roles: {
|
|
91
|
+
admin: {
|
|
92
|
+
assignable: true,
|
|
93
|
+
inherits: "member",
|
|
94
|
+
permissions: []
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}),
|
|
99
|
+
/inherits unknown role "member"/
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("createWorkspaceRoleCatalog rejects circular inherited roles", () => {
|
|
104
|
+
assert.throws(
|
|
105
|
+
() =>
|
|
106
|
+
createWorkspaceRoleCatalog({
|
|
107
|
+
roleCatalog: {
|
|
108
|
+
roles: {
|
|
109
|
+
member: {
|
|
110
|
+
assignable: true,
|
|
111
|
+
inherits: "admin",
|
|
112
|
+
permissions: []
|
|
113
|
+
},
|
|
114
|
+
admin: {
|
|
115
|
+
assignable: true,
|
|
116
|
+
inherits: "member",
|
|
117
|
+
permissions: []
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}),
|
|
122
|
+
/circular inheritance/
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("cloneWorkspaceRoleCatalog normalizes role ids and returns detached arrays", () => {
|
|
127
|
+
const source = {
|
|
128
|
+
collaborationEnabled: true,
|
|
129
|
+
defaultInviteRole: "member",
|
|
130
|
+
roles: [
|
|
131
|
+
{
|
|
132
|
+
id: " MEMBER ",
|
|
133
|
+
assignable: true,
|
|
134
|
+
permissions: ["workspace.members.view"]
|
|
135
|
+
}
|
|
136
|
+
],
|
|
137
|
+
assignableRoleIds: ["member"]
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const cloned = cloneWorkspaceRoleCatalog(source);
|
|
141
|
+
assert.deepEqual(cloned, {
|
|
142
|
+
collaborationEnabled: true,
|
|
143
|
+
defaultInviteRole: "member",
|
|
144
|
+
roles: [
|
|
145
|
+
{
|
|
146
|
+
id: "member",
|
|
147
|
+
assignable: true,
|
|
148
|
+
permissions: ["workspace.members.view"]
|
|
149
|
+
}
|
|
150
|
+
],
|
|
151
|
+
assignableRoleIds: ["member"]
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
cloned.roles[0].permissions.push("workspace.members.manage");
|
|
155
|
+
cloned.assignableRoleIds.push("admin");
|
|
156
|
+
|
|
157
|
+
assert.deepEqual(source.roles[0].permissions, ["workspace.members.view"]);
|
|
158
|
+
assert.deepEqual(source.assignableRoleIds, ["member"]);
|
|
159
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { Type } from "@fastify/type-provider-typebox";
|
|
4
|
+
import { compileRouteValidator } from "@jskit-ai/kernel/_testable";
|
|
5
|
+
import { routeParamsValidator } from "../src/server/common/validators/routeParamsValidator.js";
|
|
6
|
+
|
|
7
|
+
test("routeParamsValidator exposes a shared workspace route params validator", () => {
|
|
8
|
+
assert.equal(typeof routeParamsValidator.schema, "object");
|
|
9
|
+
assert.equal(typeof routeParamsValidator.normalize, "function");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("workspace route validator pipeline uses the shared params validator and merges query arrays automatically", () => {
|
|
13
|
+
const paginationQueryValidator = Object.freeze({
|
|
14
|
+
schema: Type.Object(
|
|
15
|
+
{
|
|
16
|
+
cursor: Type.Optional(Type.String({ minLength: 1 })),
|
|
17
|
+
limit: Type.Optional(Type.String({ pattern: "^[0-9]+$" }))
|
|
18
|
+
},
|
|
19
|
+
{ additionalProperties: false }
|
|
20
|
+
)
|
|
21
|
+
});
|
|
22
|
+
const searchQueryValidator = Object.freeze({
|
|
23
|
+
schema: Type.Object(
|
|
24
|
+
{
|
|
25
|
+
search: Type.Optional(Type.String({ minLength: 1 }))
|
|
26
|
+
},
|
|
27
|
+
{ additionalProperties: false }
|
|
28
|
+
)
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const compiled = compileRouteValidator({
|
|
32
|
+
paramsValidator: routeParamsValidator,
|
|
33
|
+
queryValidator: [paginationQueryValidator, searchQueryValidator]
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
assert.equal(compiled.schema.params.type, "object");
|
|
37
|
+
assert.equal(compiled.schema.params.additionalProperties, false);
|
|
38
|
+
assert.equal(typeof compiled.schema.params.properties.workspaceSlug, "object");
|
|
39
|
+
assert.equal(typeof compiled.schema.params.properties.memberUserId, "object");
|
|
40
|
+
assert.equal(typeof compiled.schema.params.properties.inviteId, "object");
|
|
41
|
+
assert.equal(typeof compiled.schema.params.properties.provider, "object");
|
|
42
|
+
assert.equal(compiled.input.params({ workspaceSlug: " ACME " }).workspaceSlug, "acme");
|
|
43
|
+
|
|
44
|
+
assert.equal(compiled.schema.querystring.type, "object");
|
|
45
|
+
assert.equal(compiled.schema.querystring.additionalProperties, false);
|
|
46
|
+
assert.equal(typeof compiled.schema.querystring.properties.cursor, "object");
|
|
47
|
+
assert.equal(typeof compiled.schema.querystring.properties.limit, "object");
|
|
48
|
+
assert.equal(typeof compiled.schema.querystring.properties.search, "object");
|
|
49
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
async function importWithIdentity(url, identity) {
|
|
5
|
+
return import(`${url.href}?identity=${identity}`);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
test("workspace settings field registries stay shared across module identities", async () => {
|
|
9
|
+
const workspaceModuleUrl = new URL("../src/shared/resources/workspaceSettingsFields.js", import.meta.url);
|
|
10
|
+
|
|
11
|
+
const workspaceA = await importWithIdentity(workspaceModuleUrl, "workspace-a");
|
|
12
|
+
const workspaceB = await importWithIdentity(workspaceModuleUrl, "workspace-b");
|
|
13
|
+
assert.equal(workspaceA.workspaceSettingsFields, workspaceB.workspaceSettingsFields);
|
|
14
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
TENANCY_MODE_NONE,
|
|
5
|
+
TENANCY_MODE_PERSONAL,
|
|
6
|
+
TENANCY_MODE_WORKSPACES,
|
|
7
|
+
WORKSPACE_SLUG_POLICY_NONE,
|
|
8
|
+
WORKSPACE_SLUG_POLICY_IMMUTABLE_USERNAME,
|
|
9
|
+
WORKSPACE_SLUG_POLICY_USER_SELECTED,
|
|
10
|
+
resolveTenancyProfile,
|
|
11
|
+
isWorkspacesTenancyMode
|
|
12
|
+
} from "../src/shared/tenancyProfile.js";
|
|
13
|
+
|
|
14
|
+
test("resolveTenancyProfile returns mode-specific workspace policy matrix", () => {
|
|
15
|
+
const noneProfile = resolveTenancyProfile({ tenancyMode: TENANCY_MODE_NONE });
|
|
16
|
+
assert.deepEqual(noneProfile, {
|
|
17
|
+
mode: TENANCY_MODE_NONE,
|
|
18
|
+
workspace: {
|
|
19
|
+
enabled: false,
|
|
20
|
+
autoProvision: false,
|
|
21
|
+
allowSelfCreate: false,
|
|
22
|
+
slugPolicy: WORKSPACE_SLUG_POLICY_NONE
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const personalProfile = resolveTenancyProfile({ tenancyMode: TENANCY_MODE_PERSONAL });
|
|
27
|
+
assert.deepEqual(personalProfile, {
|
|
28
|
+
mode: TENANCY_MODE_PERSONAL,
|
|
29
|
+
workspace: {
|
|
30
|
+
enabled: true,
|
|
31
|
+
autoProvision: true,
|
|
32
|
+
allowSelfCreate: false,
|
|
33
|
+
slugPolicy: WORKSPACE_SLUG_POLICY_IMMUTABLE_USERNAME
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const workspaceProfile = resolveTenancyProfile({ tenancyMode: TENANCY_MODE_WORKSPACES });
|
|
38
|
+
assert.deepEqual(workspaceProfile, {
|
|
39
|
+
mode: TENANCY_MODE_WORKSPACES,
|
|
40
|
+
workspace: {
|
|
41
|
+
enabled: true,
|
|
42
|
+
autoProvision: false,
|
|
43
|
+
allowSelfCreate: false,
|
|
44
|
+
slugPolicy: WORKSPACE_SLUG_POLICY_USER_SELECTED
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("isWorkspacesTenancyMode is true only for workspace mode", () => {
|
|
50
|
+
assert.equal(isWorkspacesTenancyMode(TENANCY_MODE_WORKSPACES), true);
|
|
51
|
+
assert.equal(isWorkspacesTenancyMode(TENANCY_MODE_PERSONAL), false);
|
|
52
|
+
assert.equal(isWorkspacesTenancyMode(TENANCY_MODE_NONE), false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("resolveTenancyProfile allows explicit workspace self-create policy override", () => {
|
|
56
|
+
const workspaceProfile = resolveTenancyProfile({
|
|
57
|
+
tenancyMode: TENANCY_MODE_WORKSPACES,
|
|
58
|
+
tenancyPolicy: {
|
|
59
|
+
workspace: {
|
|
60
|
+
allowSelfCreate: true
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
assert.equal(workspaceProfile.mode, TENANCY_MODE_WORKSPACES);
|
|
66
|
+
assert.equal(workspaceProfile.workspace.allowSelfCreate, true);
|
|
67
|
+
});
|