@jskit-ai/workspaces-core 0.1.30 → 0.1.32
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 +11 -22
- package/package.json +11 -9
- package/src/server/WorkspacesCoreServiceProvider.js +22 -2
- package/src/server/common/repositories/workspaceInvitesRepository.js +233 -78
- package/src/server/common/repositories/workspaceMembershipsRepository.js +177 -86
- package/src/server/common/repositories/workspacesRepository.js +179 -86
- package/src/server/common/services/workspaceContextService.js +26 -24
- package/src/server/common/validators/routeParamsValidator.js +36 -53
- package/src/server/registerWorkspaceCore.js +6 -7
- package/src/server/registerWorkspaceRepositories.js +7 -3
- package/src/server/support/workspaceServerScopeSupport.js +1 -1
- package/src/server/workspaceBootstrapContributor.js +5 -14
- package/src/server/workspaceDirectory/bootWorkspaceDirectoryRoutes.js +54 -27
- package/src/server/workspaceDirectory/workspaceDirectoryActions.js +30 -24
- package/src/server/workspaceMembers/bootWorkspaceMembers.js +70 -32
- package/src/server/workspaceMembers/workspaceMembersActions.js +61 -27
- package/src/server/workspaceMembers/workspaceMembersService.js +43 -7
- package/src/server/workspacePendingInvitations/bootWorkspacePendingInvitations.js +28 -13
- package/src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js +13 -15
- package/src/server/workspacePendingInvitations/workspacePendingInvitationsService.js +33 -10
- package/src/server/workspaceSettings/bootWorkspaceSettings.js +32 -13
- package/src/server/workspaceSettings/registerWorkspaceSettings.js +5 -1
- package/src/server/workspaceSettings/workspaceSettingsActions.js +18 -12
- package/src/server/workspaceSettings/workspaceSettingsRepository.js +104 -91
- package/src/server/workspaceSettings/workspaceSettingsService.js +5 -6
- package/src/shared/jsonApiTransports.js +79 -0
- package/src/shared/resources/workspaceInvitesResource.js +158 -0
- package/src/shared/resources/workspaceMembersResource.js +176 -311
- package/src/shared/resources/workspaceMembershipsResource.js +96 -0
- package/src/shared/resources/workspacePendingInvitationsResource.js +25 -72
- package/src/shared/resources/workspaceResource.js +113 -144
- package/src/shared/resources/workspaceRoleCatalogSchema.js +31 -0
- package/src/shared/resources/workspaceSettingsResource.js +276 -148
- package/test/repositoryContracts.test.js +16 -4
- package/test/resourcesCanonical.test.js +39 -16
- package/test/routeParamsValidator.test.js +37 -19
- package/test/usersRouteResources.test.js +27 -17
- package/test/workspaceActionContextContributor.test.js +1 -1
- package/test/workspaceInternalCrudResources.test.js +98 -0
- package/test/workspaceInvitesRepository.test.js +196 -148
- package/test/workspaceMembersResource.test.js +35 -0
- package/test/workspaceMembershipsRepository.test.js +155 -115
- package/test/workspacePendingInvitationsResource.test.js +18 -23
- package/test/workspacePendingInvitationsService.test.js +2 -1
- package/test/workspaceServerScopeSupport.test.js +21 -3
- package/test/workspaceSettingsActions.test.js +5 -7
- package/test/workspaceSettingsInternalResource.test.js +8 -0
- package/test/workspaceSettingsRepository.test.js +158 -123
- package/test/workspaceSettingsResource.test.js +51 -62
- package/test/workspaceSettingsService.test.js +0 -1
- package/test/workspacesRepository.test.js +318 -174
- package/test/workspacesRouteRequestInputValidator.test.js +25 -11
- package/src/server/common/resources/workspaceInvitesResource.js +0 -207
- package/src/server/common/resources/workspaceMembershipsResource.js +0 -154
- package/src/server/common/resources/workspacesResource.js +0 -170
- package/src/server/common/validators/authenticatedUserValidator.js +0 -43
- package/src/shared/resources/resolveGlobalArrayRegistry.js +0 -6
- package/src/shared/resources/workspaceSettingsFields.js +0 -65
- package/templates/packages/main/src/shared/resources/workspaceSettingsFields.js +0 -197
- package/test/settingsFieldRegistriesSingleton.test.js +0 -14
- package/test-support/registerDefaultSettingsFields.js +0 -1
|
@@ -4,7 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { existsSync } from "node:fs";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { deriveResourceRequiredMetadata } from "@jskit-ai/kernel/_testable";
|
|
7
|
-
import "
|
|
7
|
+
import { resolveStructuredSchemaTransportSchema } from "@jskit-ai/kernel/shared/validators";
|
|
8
8
|
import { workspaceMembersResource } from "../src/shared/resources/workspaceMembersResource.js";
|
|
9
9
|
import { workspaceResource } from "../src/shared/resources/workspaceResource.js";
|
|
10
10
|
import { workspaceSettingsResource } from "../src/shared/resources/workspaceSettingsResource.js";
|
|
@@ -28,15 +28,18 @@ function assertResourceShape(resource, label) {
|
|
|
28
28
|
`${label}.operations.${operationName} must resolve messages from operation.messages or resource.messages.`
|
|
29
29
|
);
|
|
30
30
|
assert.equal(
|
|
31
|
-
typeof operation.
|
|
31
|
+
typeof resolveStructuredSchemaTransportSchema(operation.output, {
|
|
32
|
+
context: `${label}.operations.${operationName}.output`,
|
|
33
|
+
defaultMode: "replace"
|
|
34
|
+
}),
|
|
32
35
|
"object",
|
|
33
36
|
`${label}.operations.${operationName} payload schema is required.`
|
|
34
37
|
);
|
|
35
38
|
}
|
|
36
39
|
|
|
37
|
-
assert.equal(typeof resource.operations.create.
|
|
38
|
-
assert.equal(typeof resource.operations.replace.
|
|
39
|
-
assert.equal(typeof resource.operations.patch.
|
|
40
|
+
assert.equal(typeof resource.operations.create.body?.schema, "object", `${label}.operations.create.body.schema is required.`);
|
|
41
|
+
assert.equal(typeof resource.operations.replace.body?.schema, "object", `${label}.operations.replace.body.schema is required.`);
|
|
42
|
+
assert.equal(typeof resource.operations.patch.body?.schema, "object", `${label}.operations.patch.body.schema is required.`);
|
|
40
43
|
|
|
41
44
|
const requiredMetadata = deriveResourceRequiredMetadata(resource);
|
|
42
45
|
assert.ok(Array.isArray(requiredMetadata.create), `${label}.derivedRequired.create must be an array.`);
|
|
@@ -69,15 +72,22 @@ test("workspace settings and invite operations expose canonical validators", ()
|
|
|
69
72
|
|
|
70
73
|
for (const { label, operation } of operationSpecs) {
|
|
71
74
|
assert.equal(typeof operation?.method, "string", `${label}.method must exist.`);
|
|
72
|
-
assert.equal(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
assert.equal(
|
|
76
|
+
typeof resolveStructuredSchemaTransportSchema(operation?.output, {
|
|
77
|
+
context: `${label}.output`,
|
|
78
|
+
defaultMode: "replace"
|
|
79
|
+
}),
|
|
80
|
+
"object",
|
|
81
|
+
`${label}.output transport schema must exist.`
|
|
82
|
+
);
|
|
83
|
+
if (operation?.body) {
|
|
84
|
+
assert.equal(typeof operation.body.schema, "object", `${label}.body.schema must exist.`);
|
|
75
85
|
}
|
|
76
|
-
if (operation?.
|
|
77
|
-
assert.equal(typeof operation.
|
|
86
|
+
if (operation?.params) {
|
|
87
|
+
assert.equal(typeof operation.params.schema, "object", `${label}.params.schema must exist.`);
|
|
78
88
|
}
|
|
79
|
-
if (operation?.
|
|
80
|
-
assert.equal(typeof operation.
|
|
89
|
+
if (operation?.query) {
|
|
90
|
+
assert.equal(typeof operation.query.schema, "object", `${label}.query.schema must exist.`);
|
|
81
91
|
}
|
|
82
92
|
}
|
|
83
93
|
});
|
|
@@ -85,13 +95,13 @@ test("workspace settings and invite operations expose canonical validators", ()
|
|
|
85
95
|
test("workspaces-core no longer uses a workspace schema helper that exposes raw schema leaves", () => {
|
|
86
96
|
const testFilePath = fileURLToPath(import.meta.url);
|
|
87
97
|
const packageRoot = path.resolve(path.dirname(testFilePath), "..");
|
|
88
|
-
const
|
|
89
|
-
assert.equal(existsSync(
|
|
98
|
+
const workspaceRoutesFilePath = path.join(packageRoot, "src", "server", "common", "routes", "workspaceRoutes.js");
|
|
99
|
+
assert.equal(existsSync(workspaceRoutesFilePath), false, "workspaceRoutes.js must not exist.");
|
|
90
100
|
});
|
|
91
101
|
|
|
92
|
-
test("workspaces-core route validators
|
|
102
|
+
test("workspaces-core route validators do not live under src/shared/schema", () => {
|
|
93
103
|
const testFilePath = fileURLToPath(import.meta.url);
|
|
94
104
|
const packageRoot = path.resolve(path.dirname(testFilePath), "..");
|
|
95
|
-
const
|
|
96
|
-
assert.equal(existsSync(
|
|
105
|
+
const sharedSchemaDirPath = path.join(packageRoot, "src", "shared", "schema");
|
|
106
|
+
assert.equal(existsSync(sharedSchemaDirPath), false, "src/shared/schema must not exist.");
|
|
97
107
|
});
|
|
@@ -181,7 +181,7 @@ test("workspace action context contributor always resolves and stores resolved c
|
|
|
181
181
|
});
|
|
182
182
|
});
|
|
183
183
|
|
|
184
|
-
test("workspace action context contributor resolves context for workspace-visible routes
|
|
184
|
+
test("workspace action context contributor resolves context for workspace-visible routes without an explicit action list", async () => {
|
|
185
185
|
const calls = [];
|
|
186
186
|
const contributor = createWorkspaceActionContextContributor({
|
|
187
187
|
workspaceService: {
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { validateOperationSection } from "@jskit-ai/http-runtime/shared/validators/operationValidation";
|
|
4
|
+
import { resolveStructuredSchemaTransportSchema } from "@jskit-ai/kernel/shared/validators";
|
|
5
|
+
import { workspaceInvitesResource } from "../src/shared/resources/workspaceInvitesResource.js";
|
|
6
|
+
import { workspaceMembershipsResource } from "../src/shared/resources/workspaceMembershipsResource.js";
|
|
7
|
+
|
|
8
|
+
function parseBody(operation, payload = {}) {
|
|
9
|
+
return validateOperationSection({
|
|
10
|
+
operation,
|
|
11
|
+
section: "body",
|
|
12
|
+
value: payload
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
test("workspace internal CRUD resources expose canonical derived operation sets", () => {
|
|
17
|
+
const resources = {
|
|
18
|
+
workspaceMemberships: workspaceMembershipsResource,
|
|
19
|
+
workspaceInvites: workspaceInvitesResource
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
for (const [label, resource] of Object.entries(resources)) {
|
|
23
|
+
assert.deepEqual(Object.keys(resource.operations), ["list", "view", "create", "patch"]);
|
|
24
|
+
assert.equal(typeof resource.operations.create.body?.schema, "object", `${label}.operations.create.body.schema is required.`);
|
|
25
|
+
assert.equal(typeof resource.operations.patch.body?.schema, "object", `${label}.operations.patch.body.schema is required.`);
|
|
26
|
+
assert.equal(
|
|
27
|
+
typeof resolveStructuredSchemaTransportSchema(resource.operations.view.output, {
|
|
28
|
+
context: `${label}.operations.view.output`,
|
|
29
|
+
defaultMode: "replace"
|
|
30
|
+
}),
|
|
31
|
+
"object",
|
|
32
|
+
`${label}.operations.view.output transport schema is required.`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("workspace memberships derived bodies accept normalized internal writes", async () => {
|
|
38
|
+
const create = await parseBody(workspaceMembershipsResource.operations.create, {
|
|
39
|
+
workspaceId: "7",
|
|
40
|
+
userId: "9",
|
|
41
|
+
roleSid: "OWNER",
|
|
42
|
+
status: "ACTIVE",
|
|
43
|
+
createdAt: "2026-05-02T10:11:12.000Z",
|
|
44
|
+
updatedAt: "2026-05-02T10:11:12.000Z"
|
|
45
|
+
});
|
|
46
|
+
assert.equal(create.ok, true);
|
|
47
|
+
assert.equal(String(create.value.workspaceId), "7");
|
|
48
|
+
assert.equal(String(create.value.userId), "9");
|
|
49
|
+
assert.equal(create.value.roleSid, "OWNER");
|
|
50
|
+
assert.equal(create.value.status, "ACTIVE");
|
|
51
|
+
assert.equal(typeof create.value.createdAt, "object");
|
|
52
|
+
assert.equal(typeof create.value.updatedAt, "object");
|
|
53
|
+
|
|
54
|
+
const patch = await parseBody(workspaceMembershipsResource.operations.patch, {
|
|
55
|
+
roleSid: "ADMIN",
|
|
56
|
+
status: "ACTIVE",
|
|
57
|
+
updatedAt: "2026-05-02T10:11:12.000Z"
|
|
58
|
+
});
|
|
59
|
+
assert.equal(patch.ok, true);
|
|
60
|
+
assert.equal(patch.value.roleSid, "ADMIN");
|
|
61
|
+
assert.equal(patch.value.status, "ACTIVE");
|
|
62
|
+
assert.equal(typeof patch.value.updatedAt, "object");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("workspace invites derived bodies keep lifecycle fields available for internal writes", async () => {
|
|
66
|
+
const create = await parseBody(workspaceInvitesResource.operations.create, {
|
|
67
|
+
workspaceId: "7",
|
|
68
|
+
email: "Invitee@Example.com",
|
|
69
|
+
roleSid: "ADMIN",
|
|
70
|
+
status: "PENDING",
|
|
71
|
+
tokenHash: " invite-token-hash ",
|
|
72
|
+
invitedByUserId: "9",
|
|
73
|
+
expiresAt: "2026-05-10T00:00:00.000Z",
|
|
74
|
+
acceptedAt: null,
|
|
75
|
+
revokedAt: null,
|
|
76
|
+
createdAt: "2026-05-02T10:11:12.000Z",
|
|
77
|
+
updatedAt: "2026-05-02T10:11:12.000Z"
|
|
78
|
+
});
|
|
79
|
+
assert.equal(create.ok, true);
|
|
80
|
+
assert.equal(String(create.value.workspaceId), "7");
|
|
81
|
+
assert.equal(create.value.email, "Invitee@Example.com");
|
|
82
|
+
assert.equal(create.value.roleSid, "ADMIN");
|
|
83
|
+
assert.equal(create.value.status, "PENDING");
|
|
84
|
+
assert.equal(create.value.tokenHash, "invite-token-hash");
|
|
85
|
+
assert.equal(String(create.value.invitedByUserId), "9");
|
|
86
|
+
assert.equal(typeof create.value.createdAt, "object");
|
|
87
|
+
assert.equal(typeof create.value.updatedAt, "object");
|
|
88
|
+
|
|
89
|
+
const patch = await parseBody(workspaceInvitesResource.operations.patch, {
|
|
90
|
+
status: "ACCEPTED",
|
|
91
|
+
acceptedAt: "2026-05-11T00:00:00.000Z",
|
|
92
|
+
updatedAt: "2026-05-11T00:00:00.000Z"
|
|
93
|
+
});
|
|
94
|
+
assert.equal(patch.ok, true);
|
|
95
|
+
assert.equal(patch.value.status, "ACCEPTED");
|
|
96
|
+
assert.equal(typeof patch.value.acceptedAt, "object");
|
|
97
|
+
assert.equal(typeof patch.value.updatedAt, "object");
|
|
98
|
+
});
|
|
@@ -1,90 +1,151 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
-
import { toIsoString
|
|
3
|
+
import { toIsoString } from "@jskit-ai/database-runtime/shared";
|
|
4
4
|
import { createRepository } from "../src/server/common/repositories/workspaceInvitesRepository.js";
|
|
5
5
|
|
|
6
|
-
function createKnexStub({
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
function createKnexStub() {
|
|
7
|
+
const knex = Object.assign(() => {
|
|
8
|
+
throw new Error("query execution not expected");
|
|
9
|
+
}, {
|
|
10
|
+
async transaction(work) {
|
|
11
|
+
return work({ trxId: "trx-1" });
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
return knex;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function toWorkspaceInviteResource(row = {}, { includeWorkspace = false } = {}) {
|
|
19
|
+
return {
|
|
20
|
+
type: "workspaceInvites",
|
|
21
|
+
id: String(row.id || ""),
|
|
22
|
+
attributes: {
|
|
23
|
+
email: row.email,
|
|
24
|
+
roleSid: row.roleSid,
|
|
25
|
+
status: row.status,
|
|
26
|
+
tokenHash: row.tokenHash,
|
|
27
|
+
expiresAt: row.expiresAt,
|
|
28
|
+
acceptedAt: row.acceptedAt,
|
|
29
|
+
revokedAt: row.revokedAt,
|
|
30
|
+
createdAt: row.createdAt,
|
|
31
|
+
updatedAt: row.updatedAt
|
|
32
|
+
},
|
|
33
|
+
relationships: {
|
|
34
|
+
workspace: {
|
|
35
|
+
data: row?.workspace?.id == null
|
|
36
|
+
? null
|
|
37
|
+
: {
|
|
38
|
+
type: "workspaces",
|
|
39
|
+
id: String(row.workspace.id)
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
invitedByUser: {
|
|
43
|
+
data: row?.invitedByUser?.id == null ? null : { type: "userProfiles", id: String(row.invitedByUser.id) }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function createWorkspaceInvitesApiStub({
|
|
50
|
+
rows = [],
|
|
51
|
+
rowById = new Map()
|
|
10
52
|
} = {}) {
|
|
11
53
|
const state = {
|
|
12
|
-
|
|
13
|
-
|
|
54
|
+
postPayload: null,
|
|
55
|
+
patchPayloads: []
|
|
14
56
|
};
|
|
15
57
|
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
58
|
+
const api = {
|
|
59
|
+
resources: {
|
|
60
|
+
workspaceInvites: {
|
|
61
|
+
async query({ queryParams }) {
|
|
62
|
+
const filters = queryParams?.filters || {};
|
|
63
|
+
const includeWorkspace = Array.isArray(queryParams?.include) && queryParams.include.includes("workspace");
|
|
64
|
+
const matching = rows.filter((row) => {
|
|
65
|
+
if (Object.hasOwn(filters, "id") && String(row.id) !== String(filters.id)) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
if (Object.hasOwn(filters, "workspace") && String(row?.workspace?.id || "") !== String(filters.workspace)) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
if (Object.hasOwn(filters, "email") && String(row.email || "") !== String(filters.email)) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
if (Object.hasOwn(filters, "status") && String(row.status || "") !== String(filters.status)) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
if (Object.hasOwn(filters, "tokenHash") && String(row.tokenHash || "") !== String(filters.tokenHash)) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
return true;
|
|
81
|
+
});
|
|
30
82
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
83
|
+
return {
|
|
84
|
+
data: matching.map((row) => toWorkspaceInviteResource(row, { includeWorkspace })),
|
|
85
|
+
included: includeWorkspace
|
|
86
|
+
? matching
|
|
87
|
+
.filter((row) => row?.workspace?.id != null)
|
|
88
|
+
.map((row) => ({
|
|
89
|
+
type: "workspaces",
|
|
90
|
+
id: String(row.workspace.id),
|
|
91
|
+
attributes: {
|
|
92
|
+
slug: row.workspace.slug,
|
|
93
|
+
name: row.workspace.name,
|
|
94
|
+
avatarUrl: row.workspace.avatarUrl
|
|
95
|
+
}
|
|
96
|
+
}))
|
|
97
|
+
: []
|
|
98
|
+
};
|
|
36
99
|
},
|
|
37
|
-
|
|
38
|
-
|
|
100
|
+
async post(payload) {
|
|
101
|
+
assert.equal(payload?.simplified, false);
|
|
102
|
+
const inputRecord = payload?.inputRecord?.data || {};
|
|
103
|
+
state.postPayload = inputRecord;
|
|
104
|
+
const row = {
|
|
105
|
+
id: "1",
|
|
106
|
+
workspace: { id: String(inputRecord.relationships?.workspace?.data?.id || "") },
|
|
107
|
+
email: inputRecord.attributes?.email,
|
|
108
|
+
roleSid: inputRecord.attributes?.roleSid,
|
|
109
|
+
status: inputRecord.attributes?.status,
|
|
110
|
+
tokenHash: inputRecord.attributes?.tokenHash,
|
|
111
|
+
invitedByUser: inputRecord.relationships?.invitedByUser?.data
|
|
112
|
+
? { id: String(inputRecord.relationships.invitedByUser.data.id) }
|
|
113
|
+
: null,
|
|
114
|
+
expiresAt: inputRecord.attributes?.expiresAt,
|
|
115
|
+
acceptedAt: inputRecord.attributes?.acceptedAt,
|
|
116
|
+
revokedAt: inputRecord.attributes?.revokedAt,
|
|
117
|
+
createdAt: "2026-03-09 00:26:35.710",
|
|
118
|
+
updatedAt: "2026-03-09 00:26:35.710"
|
|
119
|
+
};
|
|
120
|
+
rows.push(row);
|
|
121
|
+
rowById.set("1", row);
|
|
122
|
+
return { data: toWorkspaceInviteResource(row) };
|
|
39
123
|
},
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
124
|
+
async patch(payload) {
|
|
125
|
+
assert.equal(payload?.simplified, false);
|
|
126
|
+
const inputRecord = payload?.inputRecord?.data || {};
|
|
127
|
+
state.patchPayloads.push(inputRecord);
|
|
128
|
+
const existing = rowById.get(String(inputRecord.id));
|
|
129
|
+
if (existing) {
|
|
130
|
+
const updated = {
|
|
131
|
+
...existing,
|
|
132
|
+
...(inputRecord.attributes || {})
|
|
133
|
+
};
|
|
134
|
+
rowById.set(String(inputRecord.id), updated);
|
|
135
|
+
}
|
|
136
|
+
const updatedRow = rowById.get(String(inputRecord.id)) || null;
|
|
137
|
+
return updatedRow ? { data: toWorkspaceInviteResource(updatedRow) } : null;
|
|
45
138
|
}
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
assert.equal(tableName, "workspace_invites");
|
|
50
|
-
const query = {
|
|
51
|
-
criteriaList: [],
|
|
52
|
-
select() {
|
|
53
|
-
return this;
|
|
54
|
-
},
|
|
55
|
-
insert(payload) {
|
|
56
|
-
state.insertPayload = payload;
|
|
57
|
-
return Promise.resolve([1]);
|
|
58
|
-
},
|
|
59
|
-
where(criteria) {
|
|
60
|
-
this.criteriaList.push(criteria);
|
|
61
|
-
return this;
|
|
62
|
-
},
|
|
63
|
-
orderBy() {
|
|
64
|
-
return this;
|
|
65
|
-
},
|
|
66
|
-
update(payload) {
|
|
67
|
-
state.updatePayload = payload;
|
|
68
|
-
return Promise.resolve(1);
|
|
69
|
-
},
|
|
70
|
-
first() {
|
|
71
|
-
const criteria = Object.assign({}, ...this.criteriaList);
|
|
72
|
-
if (Object.hasOwn(criteria, "id")) {
|
|
73
|
-
return Promise.resolve(rowById.get(String(criteria.id)) || null);
|
|
74
|
-
}
|
|
75
|
-
return Promise.resolve({ ...defaultRow });
|
|
76
139
|
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return query;
|
|
80
|
-
}
|
|
140
|
+
}
|
|
141
|
+
};
|
|
81
142
|
|
|
82
|
-
return {
|
|
143
|
+
return { api, state };
|
|
83
144
|
}
|
|
84
145
|
|
|
85
|
-
test("workspaceInvitesRepository.insert
|
|
86
|
-
const {
|
|
87
|
-
const repository = createRepository(
|
|
146
|
+
test("workspaceInvitesRepository.insert preserves expiresAt and relationship fields through the resource write path", async () => {
|
|
147
|
+
const { api, state } = createWorkspaceInvitesApiStub();
|
|
148
|
+
const repository = createRepository({ api, knex: createKnexStub() });
|
|
88
149
|
|
|
89
150
|
await repository.insert({
|
|
90
151
|
workspaceId: "1",
|
|
@@ -96,107 +157,94 @@ test("workspaceInvitesRepository.insert normalizes expiresAt ISO input to databa
|
|
|
96
157
|
expiresAt: "2026-03-16T00:26:35.709Z"
|
|
97
158
|
});
|
|
98
159
|
|
|
99
|
-
assert.equal(state.
|
|
160
|
+
assert.equal(state.postPayload.relationships?.workspace?.data?.id, "1");
|
|
161
|
+
assert.equal(state.postPayload.attributes?.email, "invitee@example.com");
|
|
162
|
+
assert.equal(state.postPayload.relationships?.invitedByUser?.data?.id, "1");
|
|
163
|
+
assert.equal(state.postPayload.attributes?.tokenHash, "hash");
|
|
164
|
+
assert.equal(typeof state.postPayload.attributes?.expiresAt, "string");
|
|
100
165
|
});
|
|
101
166
|
|
|
102
|
-
test("workspaceInvitesRepository.findPendingByTokenHash reads from
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
created_at: "2026-03-09 00:26:35.710",
|
|
119
|
-
updated_at: "2026-03-09 00:26:35.710"
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
const repository = createRepository((tableName) => {
|
|
123
|
-
calls.tableName = String(tableName || "");
|
|
124
|
-
return {
|
|
125
|
-
select() {
|
|
126
|
-
return this;
|
|
127
|
-
},
|
|
128
|
-
where(criteria) {
|
|
129
|
-
calls.whereCriteria = criteria;
|
|
130
|
-
return this;
|
|
131
|
-
},
|
|
132
|
-
first() {
|
|
133
|
-
return Promise.resolve({ ...row });
|
|
167
|
+
test("workspaceInvitesRepository.findPendingByTokenHash reads from the canonical invite resource without workspace data", async () => {
|
|
168
|
+
const { api } = createWorkspaceInvitesApiStub({
|
|
169
|
+
rows: [
|
|
170
|
+
{
|
|
171
|
+
id: "44",
|
|
172
|
+
workspace: { id: "9" },
|
|
173
|
+
email: "invitee@example.com",
|
|
174
|
+
roleSid: "member",
|
|
175
|
+
status: "pending",
|
|
176
|
+
tokenHash: "hash-token",
|
|
177
|
+
invitedByUser: { id: "1" },
|
|
178
|
+
expiresAt: "2030-01-01 00:00:00.000",
|
|
179
|
+
acceptedAt: null,
|
|
180
|
+
revokedAt: null,
|
|
181
|
+
createdAt: "2026-03-09 00:26:35.710",
|
|
182
|
+
updatedAt: "2026-03-09 00:26:35.710"
|
|
134
183
|
}
|
|
135
|
-
|
|
184
|
+
]
|
|
136
185
|
});
|
|
186
|
+
const repository = createRepository({ api, knex: createKnexStub() });
|
|
137
187
|
|
|
138
188
|
const invite = await repository.findPendingByTokenHash("hash-token");
|
|
139
|
-
|
|
140
|
-
assert.deepEqual(calls.whereCriteria, {
|
|
141
|
-
token_hash: "hash-token",
|
|
142
|
-
status: "pending"
|
|
143
|
-
});
|
|
189
|
+
|
|
144
190
|
assert.equal(invite?.workspaceId, "9");
|
|
145
191
|
assert.equal(invite?.workspaceSlug, undefined);
|
|
146
192
|
});
|
|
147
193
|
|
|
148
194
|
test("workspaceInvitesRepository.markAcceptedById uses the internal invite resource for normalized patch writes", async () => {
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
195
|
+
const { api, state } = createWorkspaceInvitesApiStub({
|
|
196
|
+
rowById: new Map([
|
|
197
|
+
["1", {
|
|
198
|
+
id: "1",
|
|
199
|
+
workspace: { id: "1" },
|
|
200
|
+
email: "invitee@example.com",
|
|
201
|
+
roleSid: "member",
|
|
202
|
+
status: "accepted",
|
|
203
|
+
tokenHash: "hash",
|
|
204
|
+
invitedByUser: { id: "1" },
|
|
205
|
+
expiresAt: "2026-03-16 00:26:35.709",
|
|
206
|
+
acceptedAt: "2026-03-10 00:26:35.710",
|
|
207
|
+
revokedAt: null,
|
|
208
|
+
createdAt: "2026-03-09 00:26:35.710",
|
|
209
|
+
updatedAt: "2026-03-10 00:26:35.710"
|
|
210
|
+
}]
|
|
211
|
+
])
|
|
165
212
|
});
|
|
166
|
-
const repository = createRepository(
|
|
213
|
+
const repository = createRepository({ api, knex: createKnexStub() });
|
|
167
214
|
|
|
168
215
|
await repository.markAcceptedById("1");
|
|
169
216
|
|
|
170
|
-
|
|
171
|
-
assert.equal(
|
|
172
|
-
assert.
|
|
173
|
-
assert.equal(typeof
|
|
174
|
-
assert.match(state.updatePayload.updated_at, /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}$/);
|
|
217
|
+
const payload = state.patchPayloads[0];
|
|
218
|
+
assert.equal(payload.attributes?.status, "accepted");
|
|
219
|
+
assert.equal(typeof payload.attributes?.acceptedAt, "object");
|
|
220
|
+
assert.equal(typeof payload.attributes?.updatedAt, "object");
|
|
175
221
|
});
|
|
176
222
|
|
|
177
223
|
test("workspaceInvitesRepository.listPendingByWorkspaceIdWithWorkspace keeps workspace join fields outside the base resource contract", async () => {
|
|
178
|
-
const {
|
|
179
|
-
|
|
224
|
+
const { api } = createWorkspaceInvitesApiStub({
|
|
225
|
+
rows: [
|
|
180
226
|
{
|
|
181
|
-
id: 1,
|
|
182
|
-
|
|
227
|
+
id: "1",
|
|
228
|
+
workspace: {
|
|
229
|
+
id: "9",
|
|
230
|
+
slug: "tonymobily3",
|
|
231
|
+
name: "TonyMobily3",
|
|
232
|
+
avatarUrl: "https://example.com/avatar.png"
|
|
233
|
+
},
|
|
183
234
|
email: "invitee@example.com",
|
|
184
|
-
|
|
235
|
+
roleSid: "member",
|
|
185
236
|
status: "pending",
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
workspace_slug: "tonymobily3",
|
|
194
|
-
workspace_name: "TonyMobily3",
|
|
195
|
-
workspace_avatar_url: "https://example.com/avatar.png"
|
|
237
|
+
tokenHash: "hash-token",
|
|
238
|
+
invitedByUser: { id: "1" },
|
|
239
|
+
expiresAt: "2030-01-01 00:00:00.000",
|
|
240
|
+
acceptedAt: null,
|
|
241
|
+
revokedAt: null,
|
|
242
|
+
createdAt: "2026-03-09 00:26:35.710",
|
|
243
|
+
updatedAt: "2026-03-09 00:26:35.710"
|
|
196
244
|
}
|
|
197
245
|
]
|
|
198
246
|
});
|
|
199
|
-
const repository = createRepository(
|
|
247
|
+
const repository = createRepository({ api, knex: createKnexStub() });
|
|
200
248
|
|
|
201
249
|
const invites = await repository.listPendingByWorkspaceIdWithWorkspace("9");
|
|
202
250
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { resolveStructuredSchemaTransportSchema } from "@jskit-ai/kernel/shared/validators";
|
|
4
|
+
import { workspaceMembersResource } from "../src/shared/resources/workspaceMembersResource.js";
|
|
5
|
+
|
|
6
|
+
function resolveOutputSchema(operationName) {
|
|
7
|
+
return resolveStructuredSchemaTransportSchema(workspaceMembersResource.operations[operationName].output, {
|
|
8
|
+
context: `workspaceMembers.${operationName}.output`,
|
|
9
|
+
defaultMode: "replace"
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
test("workspace members role catalog output is explicit and nested", () => {
|
|
14
|
+
const outputSchema = resolveOutputSchema("rolesList");
|
|
15
|
+
const roleItemRef = outputSchema.properties.roles.items.allOf[0].$ref.replace(/^#\/definitions\//, "");
|
|
16
|
+
const roleItemSchema = outputSchema.definitions[roleItemRef];
|
|
17
|
+
|
|
18
|
+
assert.equal(outputSchema.type, "object");
|
|
19
|
+
assert.equal(outputSchema.additionalProperties, false);
|
|
20
|
+
assert.equal(outputSchema.properties.roles.type, "array");
|
|
21
|
+
assert.equal(outputSchema.properties.roles.items["x-json-rest-schema"]?.castType, "object");
|
|
22
|
+
assert.equal(roleItemSchema.type, "object");
|
|
23
|
+
assert.equal(roleItemSchema.properties.permissions.type, "array");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("workspace members invite mutation outputs expose their tracking ids explicitly", () => {
|
|
27
|
+
const createOutputSchema = resolveOutputSchema("createInvite");
|
|
28
|
+
const revokeOutputSchema = resolveOutputSchema("revokeInvite");
|
|
29
|
+
|
|
30
|
+
assert.equal(createOutputSchema.properties.createdInviteId.type, "string");
|
|
31
|
+
assert.equal(createOutputSchema.properties.inviteTokenPreview.type, "string");
|
|
32
|
+
assert.equal(revokeOutputSchema.properties.revokedInviteId.type, "string");
|
|
33
|
+
assert.equal(revokeOutputSchema.properties.roleCatalog["x-json-rest-schema"]?.castType, "object");
|
|
34
|
+
assert.equal(Array.isArray(revokeOutputSchema.properties.roleCatalog.allOf), true);
|
|
35
|
+
});
|