@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
|
@@ -11,8 +11,13 @@ import { createRepository as invitesRepository } from "../../src/server/common/r
|
|
|
11
11
|
import { createRepository as membershipsRepository } from "../../src/server/common/repositories/workspaceMembershipsRepository.js";
|
|
12
12
|
import { createRepository as workspacesRepository } from "../../src/server/common/repositories/workspacesRepository.js";
|
|
13
13
|
import { createService as membersService } from "../../src/server/workspaceMembers/workspaceMembersService.js";
|
|
14
|
+
import { createWorkspacesServerI18n } from "../../src/server/i18n.js";
|
|
14
15
|
import { createService as pendingService } from "../../src/server/workspacePendingInvitations/workspacePendingInvitationsService.js";
|
|
15
|
-
import {
|
|
16
|
+
import { noInviteAcceptanceParticipant } from "../../src/server/workspacePendingInvitations/noInviteAcceptanceParticipant.js";
|
|
17
|
+
import { createWorkspacePendingInvitationsActions } from "../../src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js";
|
|
18
|
+
import { createWorkspaceInviteUrlBuilder } from "../../src/server/workspaceMembers/workspaceInviteUrls.js";
|
|
19
|
+
import { renderDefaultWorkspaceInviteEmail } from "../../src/server/workspaceMembers/defaultWorkspaceInviteEmail.js";
|
|
20
|
+
import { unconfiguredWorkspaceInviteMailer } from "../../src/server/workspaceMembers/unconfiguredWorkspaceInviteMailer.js";
|
|
16
21
|
|
|
17
22
|
// The caller verifies an empty disposable database and runs package migrations.
|
|
18
23
|
// These application fixtures demonstrate composition, not production policy.
|
|
@@ -77,6 +82,8 @@ export async function verifyInvitationTransactions(t, db) {
|
|
|
77
82
|
const events = [];
|
|
78
83
|
let failDelivery = false;
|
|
79
84
|
const members = membersService({
|
|
85
|
+
workspaceInvitationsEnabled: true,
|
|
86
|
+
i18n: createWorkspacesServerI18n(),
|
|
80
87
|
workspaceInvitesRepository: inviteRepository, workspaceMembershipsRepository: memberships,
|
|
81
88
|
inviteExpiresInMs: 3600000, roleCatalog,
|
|
82
89
|
workspaceInviteMailer: mailer ? { async sendWorkspaceInvite(message, options) {
|
|
@@ -89,13 +96,20 @@ export async function verifyInvitationTransactions(t, db) {
|
|
|
89
96
|
if (failDelivery) throw new Error(`Controlled mail failure containing secret ${message.inviteUrl}`);
|
|
90
97
|
delivered.push(String(saved.id));
|
|
91
98
|
return { status: "sent", providerMessageId: "controlled-message" };
|
|
92
|
-
} } :
|
|
99
|
+
} } : unconfiguredWorkspaceInviteMailer,
|
|
100
|
+
inviteUrlBuilder: createWorkspaceInviteUrlBuilder({ appConfig: {}, env: {} }),
|
|
101
|
+
workspaceInviteEmailTemplate: renderDefaultWorkspaceInviteEmail
|
|
102
|
+
});
|
|
103
|
+
// Each test chooses the application's acceptance participant.
|
|
104
|
+
let participant = noInviteAcceptanceParticipant;
|
|
105
|
+
const pending = pendingService({
|
|
106
|
+
workspaceInvitesRepository: inviteRepository, workspaceMembershipsRepository: memberships, roleCatalog: createWorkspaceRoleCatalog(),
|
|
107
|
+
acceptanceParticipant: (...args) => participant(...args)
|
|
93
108
|
});
|
|
94
|
-
const pending = pendingService({ workspaceInvitesRepository: inviteRepository, workspaceMembershipsRepository: memberships });
|
|
95
109
|
const options = { context: { actor: user, channel: "api", permissions: ["workspace.members.invite"] } };
|
|
96
110
|
const payload = { email: user.email, roleSid: "member" };
|
|
97
111
|
const actions = createActionCatalogue({ events: { async publish(event) { events.push(event); } } });
|
|
98
|
-
actions.register({ contributorId: suffix, domain: "workspace", actions:
|
|
112
|
+
actions.register({ contributorId: suffix, domain: "workspace", actions: createWorkspacePendingInvitationsActions({ workspacePendingInvitationsService: pending }) });
|
|
99
113
|
const redeem = (prepared, decision = "accept", actor = user, extraInput = {}) => actions.execute({
|
|
100
114
|
actionId: "workspace.invite.redeem",
|
|
101
115
|
input: { token: prepared.inviteTokenPreview, decision, ...extraInput },
|
|
@@ -146,12 +160,13 @@ export async function verifyInvitationTransactions(t, db) {
|
|
|
146
160
|
assert.equal(events.length, 0);
|
|
147
161
|
}
|
|
148
162
|
return { user, owner, workspace, draftId, draft, invitation, assertUnlinked, members, pending, redeem, events,
|
|
149
|
-
delivered, options, payload, prepareOnce, applyAssociation, failDelivery() { failDelivery = true; }
|
|
163
|
+
delivered, options, payload, prepareOnce, applyAssociation, failDelivery() { failDelivery = true; },
|
|
164
|
+
participate(fn) { participant = fn; } };
|
|
150
165
|
}
|
|
151
166
|
|
|
152
167
|
await t.test("ordinary creation commits before controlled delivery and built-in acceptance", async () => {
|
|
153
168
|
const f = await fixture();
|
|
154
|
-
const created = await f.members.createInvite(f.workspace, f.owner, f.payload, f.options);
|
|
169
|
+
const created = (await f.members.createInvite(f.workspace, f.owner, f.payload, f.options)).value;
|
|
155
170
|
assert.equal(created.inviteDelivery.status, "sent");
|
|
156
171
|
assert.deepEqual(f.delivered, [created.createdInviteId]);
|
|
157
172
|
await f.redeem(created);
|
|
@@ -162,9 +177,9 @@ export async function verifyInvitationTransactions(t, db) {
|
|
|
162
177
|
|
|
163
178
|
await t.test("ordinary invitation works with an application participant and no association", async () => {
|
|
164
179
|
const f = await fixture({ mailer: false });
|
|
165
|
-
const created = await f.members.createInvite(f.workspace, f.owner, f.payload);
|
|
180
|
+
const created = (await f.members.createInvite(f.workspace, f.owner, f.payload)).value;
|
|
166
181
|
assert.equal(created.inviteDelivery.status, "mailer_unconfigured");
|
|
167
|
-
f.
|
|
182
|
+
f.participate(f.applyAssociation);
|
|
168
183
|
await f.redeem(created);
|
|
169
184
|
assert.equal((await f.invitation(created)).status, "accepted");
|
|
170
185
|
assert.equal((await f.draft()).user_id, null);
|
|
@@ -172,7 +187,7 @@ export async function verifyInvitationTransactions(t, db) {
|
|
|
172
187
|
|
|
173
188
|
await t.test("association rollback restores earlier invitation state and sends nothing", async () => {
|
|
174
189
|
const f = await fixture();
|
|
175
|
-
const previous = await f.members.createInvite(f.workspace, f.owner, f.payload);
|
|
190
|
+
const previous = (await f.members.createInvite(f.workspace, f.owner, f.payload)).value;
|
|
176
191
|
await assert.rejects(f.prepareOnce({ fail: true }), (error) => error.transactionOutcome === "rolledBack");
|
|
177
192
|
const rows = await db("workspace_invites").where({ workspace_id: f.workspace.id });
|
|
178
193
|
assert.equal(rows.length, 1);
|
|
@@ -192,7 +207,7 @@ export async function verifyInvitationTransactions(t, db) {
|
|
|
192
207
|
...receipt, email: "other@example.test", roleSid: "owner", inviteUrl: "https://untrusted.example"
|
|
193
208
|
});
|
|
194
209
|
assert.equal(sent.createdInviteId, prepared.createdInviteId);
|
|
195
|
-
f.
|
|
210
|
+
f.participate(f.applyAssociation);
|
|
196
211
|
await f.redeem(sent);
|
|
197
212
|
const linked = await f.draft();
|
|
198
213
|
assert.equal(String(linked.user_id), f.user.id);
|
|
@@ -204,7 +219,7 @@ export async function verifyInvitationTransactions(t, db) {
|
|
|
204
219
|
await t.test("participant failure rolls back membership, identity, training, scopes, audit and status", async () => {
|
|
205
220
|
const f = await fixture();
|
|
206
221
|
const prepared = await f.prepareOnce();
|
|
207
|
-
f.
|
|
222
|
+
f.participate(async (...args) => {
|
|
208
223
|
await f.applyAssociation(...args);
|
|
209
224
|
throw new Error("Controlled audit failure");
|
|
210
225
|
});
|
|
@@ -223,7 +238,7 @@ export async function verifyInvitationTransactions(t, db) {
|
|
|
223
238
|
} } });
|
|
224
239
|
const prepared = await f.prepareOnce();
|
|
225
240
|
await memberships.upsertMembership(f.workspace.id, f.user.id, { roleSid: "existing-role", status: "revoked" });
|
|
226
|
-
f.
|
|
241
|
+
f.participate(f.applyAssociation);
|
|
227
242
|
await assert.rejects(f.redeem(prepared), /final status failure/);
|
|
228
243
|
const previous = await memberships.findByWorkspaceIdAndUserId(f.workspace.id, f.user.id);
|
|
229
244
|
assert.equal(previous.roleSid, "existing-role");
|
|
@@ -238,7 +253,7 @@ export async function verifyInvitationTransactions(t, db) {
|
|
|
238
253
|
await t.test("acceptance completion failure reports committed and retains all accepted writes", async () => {
|
|
239
254
|
const f = await fixture();
|
|
240
255
|
const prepared = await f.prepareOnce();
|
|
241
|
-
f.
|
|
256
|
+
f.participate(f.applyAssociation);
|
|
242
257
|
failCompletionForMethod = "patch";
|
|
243
258
|
try {
|
|
244
259
|
await assert.rejects(f.redeem(prepared), (error) => error.transactionOutcome === "committed");
|
|
@@ -287,7 +302,7 @@ export async function verifyInvitationTransactions(t, db) {
|
|
|
287
302
|
: { review_state: state };
|
|
288
303
|
await db("test_invitation_drafts").where({ id: f.draftId }).update(patch);
|
|
289
304
|
const before = await f.draft();
|
|
290
|
-
f.
|
|
305
|
+
f.participate(f.applyAssociation);
|
|
291
306
|
await assert.rejects(f.redeem(prepared), (error) => error.transactionOutcome === "rolledBack");
|
|
292
307
|
assert.deepEqual(await f.draft(), before);
|
|
293
308
|
assert.equal(await memberships.findByWorkspaceIdAndUserId(f.workspace.id, f.user.id), null);
|
|
@@ -304,9 +319,10 @@ export async function verifyInvitationTransactions(t, db) {
|
|
|
304
319
|
if (state === "revoked") await f.members.revokeInvite(f.workspace, prepared.createdInviteId);
|
|
305
320
|
if (state === "accepted") await f.redeem(prepared);
|
|
306
321
|
let called = false;
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
322
|
+
const pending = pendingService({
|
|
323
|
+
workspaceInvitesRepository: invites, workspaceMembershipsRepository: memberships, roleCatalog: createWorkspaceRoleCatalog(),
|
|
324
|
+
acceptanceParticipant: async () => { called = true; }
|
|
325
|
+
});
|
|
310
326
|
const membershipBefore = await memberships.findByWorkspaceIdAndUserId(f.workspace.id, f.user.id);
|
|
311
327
|
await assert.rejects(pending.acceptInviteByToken({
|
|
312
328
|
user: state === "unauthenticated" ? null : state === "wrong-email" ? f.owner : f.user,
|
|
@@ -322,7 +338,7 @@ export async function verifyInvitationTransactions(t, db) {
|
|
|
322
338
|
const f = await fixture();
|
|
323
339
|
const prepared = await f.prepareOnce();
|
|
324
340
|
let called = false;
|
|
325
|
-
f.
|
|
341
|
+
f.participate(async () => { called = true; });
|
|
326
342
|
await assert.rejects(f.redeem(prepared, "accept", f.owner, { user: f.user, personId: f.draftId, roleSid: "owner" }));
|
|
327
343
|
assert.equal(called, false);
|
|
328
344
|
await f.assertUnlinked(prepared);
|
|
@@ -428,7 +444,7 @@ export async function verifyInvitationTransactions(t, db) {
|
|
|
428
444
|
const f = await fixture({ inviteRepository: repository });
|
|
429
445
|
const prepared = await f.prepareOnce();
|
|
430
446
|
let participation = 0;
|
|
431
|
-
f.
|
|
447
|
+
f.participate(async (...args) => { participation++; await f.applyAssociation(...args); });
|
|
432
448
|
const compete = () => competitor === "accept" || competitor === "refuse" ? f.redeem(prepared, competitor)
|
|
433
449
|
: competitor === "revoke" ? f.members.revokeInvite(f.workspace, prepared.createdInviteId)
|
|
434
450
|
: f.members.createInvite(f.workspace, f.owner, f.payload);
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import {
|
|
4
|
-
materializeWorkspaceActionSurfacesFromAppConfig,
|
|
5
4
|
resolveDefaultWorkspaceRouteSurfaceIdFromAppConfig,
|
|
6
5
|
resolveWorkspaceMembershipOptionalSurfaceIdsFromAppConfig
|
|
7
6
|
} from "../src/server/support/workspaceActionSurfaces.js";
|
|
@@ -40,60 +39,6 @@ test("resolveWorkspaceMembershipOptionalSurfaceIdsFromAppConfig honors explicit
|
|
|
40
39
|
assert.deepEqual(surfaceIds, ["app"]);
|
|
41
40
|
});
|
|
42
41
|
|
|
43
|
-
test("materializeWorkspaceActionSurfacesFromAppConfig resolves workspace surfaces from appConfig", () => {
|
|
44
|
-
const actionDefinitions = [
|
|
45
|
-
{
|
|
46
|
-
id: "workspace.settings.read",
|
|
47
|
-
surfacesFrom: "workspace"
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
id: "auth.session.read",
|
|
51
|
-
surfacesFrom: "enabled"
|
|
52
|
-
}
|
|
53
|
-
];
|
|
54
|
-
|
|
55
|
-
const materialized = materializeWorkspaceActionSurfacesFromAppConfig(actionDefinitions, {
|
|
56
|
-
appConfig: {
|
|
57
|
-
surfaceDefinitions: {
|
|
58
|
-
app: { id: "app", enabled: true, requiresWorkspace: true },
|
|
59
|
-
admin: { id: "admin", enabled: true, requiresWorkspace: true },
|
|
60
|
-
ops: { id: "ops", enabled: true, requiresWorkspace: false }
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
assert.deepEqual(materialized, [
|
|
66
|
-
{
|
|
67
|
-
id: "workspace.settings.read",
|
|
68
|
-
surfaces: ["app", "admin"]
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
id: "auth.session.read",
|
|
72
|
-
surfacesFrom: "enabled"
|
|
73
|
-
}
|
|
74
|
-
]);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test("materializeWorkspaceActionSurfacesFromAppConfig drops workspace actions when no workspace surfaces are enabled", () => {
|
|
78
|
-
const actionDefinitions = [
|
|
79
|
-
{
|
|
80
|
-
id: "workspace.settings.read",
|
|
81
|
-
surfacesFrom: "workspace"
|
|
82
|
-
}
|
|
83
|
-
];
|
|
84
|
-
|
|
85
|
-
const materialized = materializeWorkspaceActionSurfacesFromAppConfig(actionDefinitions, {
|
|
86
|
-
appConfig: {
|
|
87
|
-
surfaceDefinitions: {
|
|
88
|
-
app: { id: "app", enabled: true, requiresWorkspace: false },
|
|
89
|
-
ops: { id: "ops", enabled: true, requiresWorkspace: false }
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
assert.deepEqual(materialized, []);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
42
|
test("resolveDefaultWorkspaceRouteSurfaceIdFromAppConfig picks a workspace surface when app default is non-workspace", () => {
|
|
98
43
|
const surfaceId = resolveDefaultWorkspaceRouteSurfaceIdFromAppConfig({
|
|
99
44
|
surfaceDefaultId: "home",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import { createWorkspaceBootstrapContributor } from "../src/server/workspaceBootstrapContributor.js";
|
|
4
|
+
import { resolveTenancyProfile } from "../src/shared/tenancyProfile.js";
|
|
4
5
|
|
|
5
6
|
function createAuthenticatedProfile(overrides = {}) {
|
|
6
7
|
return {
|
|
@@ -41,9 +42,9 @@ test("workspace bootstrap contributor passes actor context to pending invites se
|
|
|
41
42
|
}
|
|
42
43
|
},
|
|
43
44
|
workspaceInvitationsEnabled: true,
|
|
44
|
-
|
|
45
|
+
tenancyProfile: resolveTenancyProfile({
|
|
45
46
|
tenancyMode: "workspaces"
|
|
46
|
-
}
|
|
47
|
+
})
|
|
47
48
|
});
|
|
48
49
|
|
|
49
50
|
await contributor.contribute({
|
|
@@ -84,9 +85,9 @@ test("workspace bootstrap contributor resolves workspace slug from bootstrap que
|
|
|
84
85
|
}
|
|
85
86
|
},
|
|
86
87
|
workspaceInvitationsEnabled: true,
|
|
87
|
-
|
|
88
|
+
tenancyProfile: resolveTenancyProfile({
|
|
88
89
|
tenancyMode: "workspaces"
|
|
89
|
-
}
|
|
90
|
+
})
|
|
90
91
|
});
|
|
91
92
|
|
|
92
93
|
const payload = await contributor.contribute({
|
|
@@ -129,9 +130,9 @@ test("workspace bootstrap contributor reports unauthenticated requested workspac
|
|
|
129
130
|
}
|
|
130
131
|
},
|
|
131
132
|
workspaceInvitationsEnabled: true,
|
|
132
|
-
|
|
133
|
+
tenancyProfile: resolveTenancyProfile({
|
|
133
134
|
tenancyMode: "workspaces"
|
|
134
|
-
}
|
|
135
|
+
})
|
|
135
136
|
});
|
|
136
137
|
|
|
137
138
|
const payload = await contributor.contribute({
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { WORKSPACE_CRUD_SCOPE } from "../src/server/support/workspaceCrud.js";
|
|
4
|
+
import { WORKSPACE_ROUTE_BASE_PATH, resolveWorkspaceRoutePath } from "../src/server/support/workspaceRoutePaths.js";
|
|
5
|
+
import { routeParamsValidator, workspaceSlugParamsValidator } from "../src/server/common/validators/routeParamsValidator.js";
|
|
6
|
+
|
|
7
|
+
test("the workspace CRUD scope routes under the workspace slug and reads it from the route", () => {
|
|
8
|
+
assert.ok(Object.isFrozen(WORKSPACE_CRUD_SCOPE));
|
|
9
|
+
assert.ok(Object.isFrozen(WORKSPACE_CRUD_SCOPE.inputKeys));
|
|
10
|
+
assert.equal(WORKSPACE_CRUD_SCOPE.routeBase, "/w/:workspaceSlug");
|
|
11
|
+
assert.equal(WORKSPACE_CRUD_SCOPE.actionInputValidator, workspaceSlugParamsValidator);
|
|
12
|
+
assert.equal(WORKSPACE_CRUD_SCOPE.routeParamsValidator, routeParamsValidator);
|
|
13
|
+
assert.deepEqual(WORKSPACE_CRUD_SCOPE.inputKeys, ["workspaceSlug"]);
|
|
14
|
+
assert.deepEqual(WORKSPACE_CRUD_SCOPE.input({ input: { params: { workspaceSlug: " Acme " } } }), { workspaceSlug: "acme" });
|
|
15
|
+
assert.deepEqual(WORKSPACE_CRUD_SCOPE.input({ input: { params: {} } }), {});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("workspace route paths live under /api/w/:workspaceSlug", () => {
|
|
19
|
+
assert.equal(WORKSPACE_ROUTE_BASE_PATH, "/api/w/:workspaceSlug");
|
|
20
|
+
assert.equal(resolveWorkspaceRoutePath("rewarded"), "/api/w/:workspaceSlug/rewarded");
|
|
21
|
+
assert.equal(resolveWorkspaceRoutePath("/members/"), "/api/w/:workspaceSlug/members");
|
|
22
|
+
assert.equal(resolveWorkspaceRoutePath(), "/api/w/:workspaceSlug");
|
|
23
|
+
});
|
|
@@ -50,6 +50,7 @@ function createWorkspaceInvitesApiStub({
|
|
|
50
50
|
forUpdate() { return this; },
|
|
51
51
|
then(resolve) { return Promise.resolve([...rowById.values()].filter((row) => row.status === "pending")).then(resolve); }
|
|
52
52
|
});
|
|
53
|
+
trx.isCompleted = () => false;
|
|
53
54
|
const api = {
|
|
54
55
|
async transaction(work) { return work(trx); },
|
|
55
56
|
resources: {
|
|
@@ -185,8 +186,8 @@ test("workspaceInvitesRepository.findByTokenHashWithWorkspace preserves workspac
|
|
|
185
186
|
id: "45",
|
|
186
187
|
workspace: {
|
|
187
188
|
id: "9",
|
|
188
|
-
slug: "
|
|
189
|
-
name: "
|
|
189
|
+
slug: "tony3",
|
|
190
|
+
name: "Tony3",
|
|
190
191
|
avatarUrl: "https://example.com/avatar.png"
|
|
191
192
|
},
|
|
192
193
|
email: "invitee@example.com",
|
|
@@ -208,8 +209,8 @@ test("workspaceInvitesRepository.findByTokenHashWithWorkspace preserves workspac
|
|
|
208
209
|
|
|
209
210
|
assert.equal(invite?.id, "45");
|
|
210
211
|
assert.equal(invite?.status, "accepted");
|
|
211
|
-
assert.equal(invite?.workspaceSlug, "
|
|
212
|
-
assert.equal(invite?.workspaceName, "
|
|
212
|
+
assert.equal(invite?.workspaceSlug, "tony3");
|
|
213
|
+
assert.equal(invite?.workspaceName, "Tony3");
|
|
213
214
|
assert.equal(invite?.workspaceAvatarUrl, "https://example.com/avatar.png");
|
|
214
215
|
});
|
|
215
216
|
|
|
@@ -249,8 +250,8 @@ test("workspaceInvitesRepository.listPendingByWorkspaceIdWithWorkspace keeps wor
|
|
|
249
250
|
id: "1",
|
|
250
251
|
workspace: {
|
|
251
252
|
id: "9",
|
|
252
|
-
slug: "
|
|
253
|
-
name: "
|
|
253
|
+
slug: "tony3",
|
|
254
|
+
name: "Tony3",
|
|
254
255
|
avatarUrl: "https://example.com/avatar.png"
|
|
255
256
|
},
|
|
256
257
|
email: "invitee@example.com",
|
|
@@ -284,8 +285,8 @@ test("workspaceInvitesRepository.listPendingByWorkspaceIdWithWorkspace keeps wor
|
|
|
284
285
|
revokedAt: null,
|
|
285
286
|
createdAt: toIsoString("2026-03-09 00:26:35.710"),
|
|
286
287
|
updatedAt: toIsoString("2026-03-09 00:26:35.710"),
|
|
287
|
-
workspaceSlug: "
|
|
288
|
-
workspaceName: "
|
|
288
|
+
workspaceSlug: "tony3",
|
|
289
|
+
workspaceName: "Tony3",
|
|
289
290
|
workspaceAvatarUrl: "https://example.com/avatar.png"
|
|
290
291
|
}
|
|
291
292
|
]);
|