@jskit-ai/workspaces-core 0.1.21 → 0.1.23
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 +6 -6
- package/src/server/common/repositories/workspaceInvitesRepository.js +68 -65
- package/src/server/common/repositories/workspaceMembershipsRepository.js +83 -52
- package/src/server/common/repositories/workspacesRepository.js +42 -67
- package/src/server/common/resources/workspaceInvitesResource.js +207 -0
- package/src/server/common/resources/workspaceMembershipsResource.js +154 -0
- package/src/server/common/resources/workspacesResource.js +170 -0
- package/src/server/registerWorkspaceBootstrap.js +1 -1
- package/src/server/registerWorkspaceCore.js +3 -3
- package/src/server/registerWorkspaceRepositories.js +3 -3
- package/src/server/workspaceBootstrapContributor.js +4 -4
- package/src/server/workspaceMembers/registerWorkspaceMembers.js +2 -2
- package/src/server/workspacePendingInvitations/registerWorkspacePendingInvitations.js +2 -2
- package/src/server/workspaceSettings/registerWorkspaceSettings.js +2 -2
- package/src/server/workspaceSettings/workspaceSettingsRepository.js +5 -4
- package/src/shared/resources/workspaceMembersResource.js +1 -1
- package/src/shared/resources/workspacePendingInvitationsResource.js +1 -1
- package/src/shared/resources/workspaceResource.js +1 -1
- package/src/shared/resources/workspaceSettingsFields.js +10 -4
- package/src/shared/resources/workspaceSettingsResource.js +1 -1
- package/templates/packages/main/src/shared/resources/workspaceSettingsFields.js +9 -9
- package/test/exportsContract.test.js +3 -1
- package/test/registerWorkspaceBootstrap.test.js +1 -1
- package/test/registerWorkspaceSettings.test.js +1 -1
- package/test/usersRouteResources.test.js +1 -1
- package/test/workspaceBootstrapContributor.test.js +3 -3
- package/test/workspaceInvitesRepository.test.js +129 -18
- package/test/workspaceMembershipsRepository.test.js +212 -0
- package/test/workspacesRepository.test.js +253 -0
|
@@ -68,7 +68,7 @@ const pendingInvitationsListOutputValidator = Object.freeze({
|
|
|
68
68
|
const WORKSPACE_PENDING_INVITATIONS_MESSAGES = createOperationMessages();
|
|
69
69
|
|
|
70
70
|
const workspacePendingInvitationsResource = Object.freeze({
|
|
71
|
-
|
|
71
|
+
namespace: "workspacePendingInvitations",
|
|
72
72
|
messages: WORKSPACE_PENDING_INVITATIONS_MESSAGES,
|
|
73
73
|
operations: Object.freeze({
|
|
74
74
|
list: Object.freeze({
|
|
@@ -130,7 +130,7 @@ const workspaceSummaryOutputValidator = Object.freeze({
|
|
|
130
130
|
});
|
|
131
131
|
|
|
132
132
|
const resource = {
|
|
133
|
-
|
|
133
|
+
namespace: "workspace",
|
|
134
134
|
messages: {
|
|
135
135
|
validation: "Fix invalid workspace values and try again.",
|
|
136
136
|
saveSuccess: "Workspace updated.",
|
|
@@ -17,9 +17,13 @@ function defineField(field = {}) {
|
|
|
17
17
|
if (!field.outputSchema || typeof field.outputSchema !== "object") {
|
|
18
18
|
throw new TypeError(`workspaceSettingsFields.defineField("${key}") requires outputSchema.`);
|
|
19
19
|
}
|
|
20
|
-
const
|
|
21
|
-
if (!
|
|
22
|
-
throw new TypeError(`workspaceSettingsFields.defineField("${key}") requires
|
|
20
|
+
const repository = field?.repository;
|
|
21
|
+
if (!repository || typeof repository !== "object" || Array.isArray(repository)) {
|
|
22
|
+
throw new TypeError(`workspaceSettingsFields.defineField("${key}") requires repository.column.`);
|
|
23
|
+
}
|
|
24
|
+
const repositoryColumn = normalizeText(repository.column);
|
|
25
|
+
if (!repositoryColumn) {
|
|
26
|
+
throw new TypeError(`workspaceSettingsFields.defineField("${key}") requires repository.column.`);
|
|
23
27
|
}
|
|
24
28
|
if (typeof field.normalizeInput !== "function") {
|
|
25
29
|
throw new TypeError(`workspaceSettingsFields.defineField("${key}") requires normalizeInput.`);
|
|
@@ -33,7 +37,9 @@ function defineField(field = {}) {
|
|
|
33
37
|
|
|
34
38
|
workspaceSettingsFields.push({
|
|
35
39
|
key,
|
|
36
|
-
|
|
40
|
+
repository: Object.freeze({
|
|
41
|
+
column: repositoryColumn
|
|
42
|
+
}),
|
|
37
43
|
required: field.required !== false,
|
|
38
44
|
inputSchema: field.inputSchema,
|
|
39
45
|
outputSchema: field.outputSchema,
|
|
@@ -117,7 +117,7 @@ const responseRecordValidator = Object.freeze({
|
|
|
117
117
|
});
|
|
118
118
|
|
|
119
119
|
const resource = {
|
|
120
|
-
|
|
120
|
+
namespace: "workspaceSettings",
|
|
121
121
|
messages: {
|
|
122
122
|
validation: "Fix invalid workspace settings values and try again.",
|
|
123
123
|
saveSuccess: "Workspace settings updated.",
|
|
@@ -22,7 +22,7 @@ resetWorkspaceSettingsFields();
|
|
|
22
22
|
|
|
23
23
|
defineField({
|
|
24
24
|
key: "lightPrimaryColor",
|
|
25
|
-
|
|
25
|
+
repository: { column: "light_primary_color" },
|
|
26
26
|
required: true,
|
|
27
27
|
inputSchema: Type.String({
|
|
28
28
|
minLength: 7,
|
|
@@ -42,7 +42,7 @@ defineField({
|
|
|
42
42
|
|
|
43
43
|
defineField({
|
|
44
44
|
key: "lightSecondaryColor",
|
|
45
|
-
|
|
45
|
+
repository: { column: "light_secondary_color" },
|
|
46
46
|
required: true,
|
|
47
47
|
inputSchema: Type.String({
|
|
48
48
|
minLength: 7,
|
|
@@ -62,7 +62,7 @@ defineField({
|
|
|
62
62
|
|
|
63
63
|
defineField({
|
|
64
64
|
key: "lightSurfaceColor",
|
|
65
|
-
|
|
65
|
+
repository: { column: "light_surface_color" },
|
|
66
66
|
required: true,
|
|
67
67
|
inputSchema: Type.String({
|
|
68
68
|
minLength: 7,
|
|
@@ -82,7 +82,7 @@ defineField({
|
|
|
82
82
|
|
|
83
83
|
defineField({
|
|
84
84
|
key: "lightSurfaceVariantColor",
|
|
85
|
-
|
|
85
|
+
repository: { column: "light_surface_variant_color" },
|
|
86
86
|
required: true,
|
|
87
87
|
inputSchema: Type.String({
|
|
88
88
|
minLength: 7,
|
|
@@ -102,7 +102,7 @@ defineField({
|
|
|
102
102
|
|
|
103
103
|
defineField({
|
|
104
104
|
key: "darkPrimaryColor",
|
|
105
|
-
|
|
105
|
+
repository: { column: "dark_primary_color" },
|
|
106
106
|
required: true,
|
|
107
107
|
inputSchema: Type.String({
|
|
108
108
|
minLength: 7,
|
|
@@ -122,7 +122,7 @@ defineField({
|
|
|
122
122
|
|
|
123
123
|
defineField({
|
|
124
124
|
key: "darkSecondaryColor",
|
|
125
|
-
|
|
125
|
+
repository: { column: "dark_secondary_color" },
|
|
126
126
|
required: true,
|
|
127
127
|
inputSchema: Type.String({
|
|
128
128
|
minLength: 7,
|
|
@@ -142,7 +142,7 @@ defineField({
|
|
|
142
142
|
|
|
143
143
|
defineField({
|
|
144
144
|
key: "darkSurfaceColor",
|
|
145
|
-
|
|
145
|
+
repository: { column: "dark_surface_color" },
|
|
146
146
|
required: true,
|
|
147
147
|
inputSchema: Type.String({
|
|
148
148
|
minLength: 7,
|
|
@@ -162,7 +162,7 @@ defineField({
|
|
|
162
162
|
|
|
163
163
|
defineField({
|
|
164
164
|
key: "darkSurfaceVariantColor",
|
|
165
|
-
|
|
165
|
+
repository: { column: "dark_surface_variant_color" },
|
|
166
166
|
required: true,
|
|
167
167
|
inputSchema: Type.String({
|
|
168
168
|
minLength: 7,
|
|
@@ -182,7 +182,7 @@ defineField({
|
|
|
182
182
|
|
|
183
183
|
defineField({
|
|
184
184
|
key: "invitesEnabled",
|
|
185
|
-
|
|
185
|
+
repository: { column: "invites_enabled" },
|
|
186
186
|
required: true,
|
|
187
187
|
inputSchema: Type.Boolean({
|
|
188
188
|
messages: {
|
|
@@ -14,7 +14,9 @@ test("workspaces-core exports are explicit and aligned with production usage", (
|
|
|
14
14
|
packageDir: PACKAGE_DIR,
|
|
15
15
|
packageId: "@jskit-ai/workspaces-core",
|
|
16
16
|
requiredExports: [
|
|
17
|
-
"./server/WorkspacesCoreServiceProvider"
|
|
17
|
+
"./server/WorkspacesCoreServiceProvider",
|
|
18
|
+
"./server/validators/routeParamsValidator",
|
|
19
|
+
"./server/support/workspaceRouteInput"
|
|
18
20
|
]
|
|
19
21
|
});
|
|
20
22
|
|
|
@@ -28,7 +28,7 @@ test("registerWorkspaceSettings registers workspace settings service realtime ev
|
|
|
28
28
|
|
|
29
29
|
registerWorkspaceSettings(app);
|
|
30
30
|
|
|
31
|
-
assert.equal(singletonBindings.has("
|
|
31
|
+
assert.equal(singletonBindings.has("internal.repository.workspace-settings"), true);
|
|
32
32
|
assert.equal(serviceCalls.length, 1);
|
|
33
33
|
assert.equal(serviceCalls[0].token, "workspaces.settings.service");
|
|
34
34
|
assert.equal(typeof serviceCalls[0].factory, "function");
|
|
@@ -12,7 +12,7 @@ import { workspaceSettingsResource } from "../src/shared/resources/workspaceSett
|
|
|
12
12
|
function assertResourceShape(resource, label) {
|
|
13
13
|
assert.ok(resource, `${label} resource must exist.`);
|
|
14
14
|
assert.equal(typeof resource, "object", `${label} resource must be an object.`);
|
|
15
|
-
assert.equal(typeof resource.
|
|
15
|
+
assert.equal(typeof resource.namespace, "string", `.namespace must be a string.`);
|
|
16
16
|
|
|
17
17
|
for (const operationName of ["view", "list", "create", "replace", "patch"]) {
|
|
18
18
|
const operation = resource.operations?.[operationName];
|
|
@@ -35,7 +35,7 @@ test("workspace bootstrap contributor passes actor context to pending invites se
|
|
|
35
35
|
return [];
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
|
-
|
|
38
|
+
userProfilesRepository: {
|
|
39
39
|
async findById() {
|
|
40
40
|
return profile;
|
|
41
41
|
}
|
|
@@ -78,7 +78,7 @@ test("workspace bootstrap contributor resolves workspace slug from bootstrap que
|
|
|
78
78
|
return [];
|
|
79
79
|
}
|
|
80
80
|
},
|
|
81
|
-
|
|
81
|
+
userProfilesRepository: {
|
|
82
82
|
async findById() {
|
|
83
83
|
return profile;
|
|
84
84
|
}
|
|
@@ -123,7 +123,7 @@ test("workspace bootstrap contributor reports unauthenticated requested workspac
|
|
|
123
123
|
assert.fail("listPendingInvitesForUser should not run for unauthenticated payloads");
|
|
124
124
|
}
|
|
125
125
|
},
|
|
126
|
-
|
|
126
|
+
userProfilesRepository: {
|
|
127
127
|
async findById() {
|
|
128
128
|
assert.fail("findById should not run for unauthenticated payloads");
|
|
129
129
|
}
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
+
import { toIsoString, toNullableDateTime } from "@jskit-ai/database-runtime/shared";
|
|
3
4
|
import { createRepository } from "../src/server/common/repositories/workspaceInvitesRepository.js";
|
|
4
5
|
|
|
5
|
-
function createKnexStub(
|
|
6
|
+
function createKnexStub({
|
|
7
|
+
rowById = new Map(),
|
|
8
|
+
pendingRow = null,
|
|
9
|
+
joinedRows = []
|
|
10
|
+
} = {}) {
|
|
6
11
|
const state = {
|
|
7
|
-
insertPayload: null
|
|
12
|
+
insertPayload: null,
|
|
13
|
+
updatePayload: null
|
|
8
14
|
};
|
|
9
15
|
|
|
10
|
-
const
|
|
16
|
+
const defaultRow = pendingRow || {
|
|
11
17
|
id: 1,
|
|
12
18
|
workspace_id: 1,
|
|
13
19
|
email: "invitee@example.com",
|
|
@@ -23,28 +29,54 @@ function createKnexStub() {
|
|
|
23
29
|
};
|
|
24
30
|
|
|
25
31
|
function tableBuilder(tableName) {
|
|
32
|
+
if (tableName === "workspace_invites as wi") {
|
|
33
|
+
return {
|
|
34
|
+
join() {
|
|
35
|
+
return this;
|
|
36
|
+
},
|
|
37
|
+
where() {
|
|
38
|
+
return this;
|
|
39
|
+
},
|
|
40
|
+
orderBy() {
|
|
41
|
+
return this;
|
|
42
|
+
},
|
|
43
|
+
select() {
|
|
44
|
+
return Promise.resolve([...joinedRows]);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
26
49
|
assert.equal(tableName, "workspace_invites");
|
|
27
|
-
|
|
50
|
+
const query = {
|
|
51
|
+
criteriaList: [],
|
|
52
|
+
select() {
|
|
53
|
+
return this;
|
|
54
|
+
},
|
|
28
55
|
insert(payload) {
|
|
29
56
|
state.insertPayload = payload;
|
|
30
57
|
return Promise.resolve([1]);
|
|
31
58
|
},
|
|
32
59
|
where(criteria) {
|
|
33
|
-
|
|
34
|
-
return
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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 });
|
|
46
76
|
}
|
|
47
77
|
};
|
|
78
|
+
|
|
79
|
+
return query;
|
|
48
80
|
}
|
|
49
81
|
|
|
50
82
|
return { knexStub: tableBuilder, state };
|
|
@@ -64,7 +96,7 @@ test("workspaceInvitesRepository.insert normalizes expiresAt ISO input to databa
|
|
|
64
96
|
expiresAt: "2026-03-16T00:26:35.709Z"
|
|
65
97
|
});
|
|
66
98
|
|
|
67
|
-
assert.equal(state.insertPayload.expires_at, "2026-03-
|
|
99
|
+
assert.equal(state.insertPayload.expires_at, toNullableDateTime("2026-03-16T00:26:35.709Z"));
|
|
68
100
|
});
|
|
69
101
|
|
|
70
102
|
test("workspaceInvitesRepository.findPendingByTokenHash reads from invites table without workspace join", async () => {
|
|
@@ -90,6 +122,9 @@ test("workspaceInvitesRepository.findPendingByTokenHash reads from invites table
|
|
|
90
122
|
const repository = createRepository((tableName) => {
|
|
91
123
|
calls.tableName = String(tableName || "");
|
|
92
124
|
return {
|
|
125
|
+
select() {
|
|
126
|
+
return this;
|
|
127
|
+
},
|
|
93
128
|
where(criteria) {
|
|
94
129
|
calls.whereCriteria = criteria;
|
|
95
130
|
return this;
|
|
@@ -109,3 +144,79 @@ test("workspaceInvitesRepository.findPendingByTokenHash reads from invites table
|
|
|
109
144
|
assert.equal(invite?.workspaceId, "9");
|
|
110
145
|
assert.equal(invite?.workspaceSlug, undefined);
|
|
111
146
|
});
|
|
147
|
+
|
|
148
|
+
test("workspaceInvitesRepository.markAcceptedById uses the internal invite resource for normalized patch writes", async () => {
|
|
149
|
+
const acceptedRow = {
|
|
150
|
+
id: 1,
|
|
151
|
+
workspace_id: 1,
|
|
152
|
+
email: "invitee@example.com",
|
|
153
|
+
role_sid: "member",
|
|
154
|
+
status: "accepted",
|
|
155
|
+
token_hash: "hash",
|
|
156
|
+
invited_by_user_id: 1,
|
|
157
|
+
expires_at: "2026-03-16 00:26:35.709",
|
|
158
|
+
accepted_at: "2026-03-10 00:26:35.710",
|
|
159
|
+
revoked_at: null,
|
|
160
|
+
created_at: "2026-03-09 00:26:35.710",
|
|
161
|
+
updated_at: "2026-03-10 00:26:35.710"
|
|
162
|
+
};
|
|
163
|
+
const { knexStub, state } = createKnexStub({
|
|
164
|
+
rowById: new Map([["1", acceptedRow]])
|
|
165
|
+
});
|
|
166
|
+
const repository = createRepository(knexStub);
|
|
167
|
+
|
|
168
|
+
await repository.markAcceptedById("1");
|
|
169
|
+
|
|
170
|
+
assert.equal(state.updatePayload.status, "accepted");
|
|
171
|
+
assert.equal(typeof state.updatePayload.accepted_at, "string");
|
|
172
|
+
assert.match(state.updatePayload.accepted_at, /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}$/);
|
|
173
|
+
assert.equal(typeof state.updatePayload.updated_at, "string");
|
|
174
|
+
assert.match(state.updatePayload.updated_at, /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}$/);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("workspaceInvitesRepository.listPendingByWorkspaceIdWithWorkspace keeps workspace join fields outside the base resource contract", async () => {
|
|
178
|
+
const { knexStub } = createKnexStub({
|
|
179
|
+
joinedRows: [
|
|
180
|
+
{
|
|
181
|
+
id: 1,
|
|
182
|
+
workspace_id: 9,
|
|
183
|
+
email: "invitee@example.com",
|
|
184
|
+
role_sid: "member",
|
|
185
|
+
status: "pending",
|
|
186
|
+
token_hash: "hash-token",
|
|
187
|
+
invited_by_user_id: 1,
|
|
188
|
+
expires_at: "2030-01-01 00:00:00.000",
|
|
189
|
+
accepted_at: null,
|
|
190
|
+
revoked_at: null,
|
|
191
|
+
created_at: "2026-03-09 00:26:35.710",
|
|
192
|
+
updated_at: "2026-03-09 00:26:35.710",
|
|
193
|
+
workspace_slug: "tonymobily3",
|
|
194
|
+
workspace_name: "TonyMobily3",
|
|
195
|
+
workspace_avatar_url: "https://example.com/avatar.png"
|
|
196
|
+
}
|
|
197
|
+
]
|
|
198
|
+
});
|
|
199
|
+
const repository = createRepository(knexStub);
|
|
200
|
+
|
|
201
|
+
const invites = await repository.listPendingByWorkspaceIdWithWorkspace("9");
|
|
202
|
+
|
|
203
|
+
assert.deepEqual(invites, [
|
|
204
|
+
{
|
|
205
|
+
id: "1",
|
|
206
|
+
workspaceId: "9",
|
|
207
|
+
email: "invitee@example.com",
|
|
208
|
+
roleSid: "member",
|
|
209
|
+
status: "pending",
|
|
210
|
+
tokenHash: "hash-token",
|
|
211
|
+
invitedByUserId: "1",
|
|
212
|
+
expiresAt: toIsoString("2030-01-01 00:00:00.000"),
|
|
213
|
+
acceptedAt: null,
|
|
214
|
+
revokedAt: null,
|
|
215
|
+
createdAt: toIsoString("2026-03-09 00:26:35.710"),
|
|
216
|
+
updatedAt: toIsoString("2026-03-09 00:26:35.710"),
|
|
217
|
+
workspaceSlug: "tonymobily3",
|
|
218
|
+
workspaceName: "TonyMobily3",
|
|
219
|
+
workspaceAvatarUrl: "https://example.com/avatar.png"
|
|
220
|
+
}
|
|
221
|
+
]);
|
|
222
|
+
});
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { toIsoString } from "@jskit-ai/database-runtime/shared";
|
|
4
|
+
import { createRepository } from "../src/server/common/repositories/workspaceMembershipsRepository.js";
|
|
5
|
+
|
|
6
|
+
function createKnexStub({
|
|
7
|
+
rowById = new Map(),
|
|
8
|
+
rowByComposite = new Map(),
|
|
9
|
+
memberSummaryRows = []
|
|
10
|
+
} = {}) {
|
|
11
|
+
const state = {
|
|
12
|
+
insertPayload: null,
|
|
13
|
+
updatePayload: null
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function buildMembershipsQuery(tableName) {
|
|
17
|
+
if (tableName === "workspace_memberships as wm") {
|
|
18
|
+
return {
|
|
19
|
+
join() {
|
|
20
|
+
return this;
|
|
21
|
+
},
|
|
22
|
+
where() {
|
|
23
|
+
return this;
|
|
24
|
+
},
|
|
25
|
+
orderBy() {
|
|
26
|
+
return this;
|
|
27
|
+
},
|
|
28
|
+
select() {
|
|
29
|
+
return Promise.resolve([...memberSummaryRows]);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const query = {
|
|
35
|
+
criteriaList: [],
|
|
36
|
+
select() {
|
|
37
|
+
return this;
|
|
38
|
+
},
|
|
39
|
+
insert(payload) {
|
|
40
|
+
state.insertPayload = payload;
|
|
41
|
+
const insertedRow = rowById.get("1");
|
|
42
|
+
if (insertedRow) {
|
|
43
|
+
rowByComposite.set(`${payload.workspace_id}:${payload.user_id}`, insertedRow);
|
|
44
|
+
}
|
|
45
|
+
return Promise.resolve([1]);
|
|
46
|
+
},
|
|
47
|
+
where(criteria) {
|
|
48
|
+
this.criteriaList.push(criteria);
|
|
49
|
+
return this;
|
|
50
|
+
},
|
|
51
|
+
update(payload) {
|
|
52
|
+
state.updatePayload = payload;
|
|
53
|
+
const criteria = Object.assign({}, ...this.criteriaList);
|
|
54
|
+
const existingRow = rowById.get(String(criteria.id));
|
|
55
|
+
if (existingRow) {
|
|
56
|
+
const updatedRow = {
|
|
57
|
+
...existingRow,
|
|
58
|
+
...payload
|
|
59
|
+
};
|
|
60
|
+
rowById.set(String(criteria.id), updatedRow);
|
|
61
|
+
rowByComposite.set(`${updatedRow.workspace_id}:${updatedRow.user_id}`, updatedRow);
|
|
62
|
+
}
|
|
63
|
+
return Promise.resolve(1);
|
|
64
|
+
},
|
|
65
|
+
first() {
|
|
66
|
+
const criteria = Object.assign({}, ...this.criteriaList);
|
|
67
|
+
if (Object.hasOwn(criteria, "id")) {
|
|
68
|
+
return Promise.resolve(rowById.get(String(criteria.id)) || null);
|
|
69
|
+
}
|
|
70
|
+
if (Object.hasOwn(criteria, "workspace_id") && Object.hasOwn(criteria, "user_id")) {
|
|
71
|
+
return Promise.resolve(
|
|
72
|
+
rowByComposite.get(`${criteria.workspace_id}:${criteria.user_id}`) || null
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
return Promise.resolve(null);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return query;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function knex(tableName) {
|
|
83
|
+
if (tableName === "workspace_memberships" || tableName === "workspace_memberships as wm") {
|
|
84
|
+
return buildMembershipsQuery(tableName);
|
|
85
|
+
}
|
|
86
|
+
throw new Error(`Unexpected table ${tableName}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
knex.transaction = async (work) => work(knex);
|
|
90
|
+
|
|
91
|
+
return { knex, state };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
test("workspaceMembershipsRepository.findByWorkspaceIdAndUserId normalizes canonical membership rows via the internal resource", async () => {
|
|
95
|
+
const membershipRow = {
|
|
96
|
+
id: 11,
|
|
97
|
+
workspace_id: 7,
|
|
98
|
+
user_id: 9,
|
|
99
|
+
role_sid: "owner",
|
|
100
|
+
status: "active",
|
|
101
|
+
created_at: "2026-03-09 00:26:35.710",
|
|
102
|
+
updated_at: "2026-03-10 00:26:35.710"
|
|
103
|
+
};
|
|
104
|
+
const { knex } = createKnexStub({
|
|
105
|
+
rowByComposite: new Map([["7:9", membershipRow]])
|
|
106
|
+
});
|
|
107
|
+
const repository = createRepository(knex);
|
|
108
|
+
|
|
109
|
+
const membership = await repository.findByWorkspaceIdAndUserId("7", "9");
|
|
110
|
+
|
|
111
|
+
assert.deepEqual(membership, {
|
|
112
|
+
id: "11",
|
|
113
|
+
workspaceId: "7",
|
|
114
|
+
userId: "9",
|
|
115
|
+
roleSid: "owner",
|
|
116
|
+
status: "active",
|
|
117
|
+
createdAt: toIsoString("2026-03-09 00:26:35.710"),
|
|
118
|
+
updatedAt: toIsoString("2026-03-10 00:26:35.710")
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("workspaceMembershipsRepository.ensureOwnerMembership upgrades an existing membership through the runtime update path", async () => {
|
|
123
|
+
const existingRow = {
|
|
124
|
+
id: 11,
|
|
125
|
+
workspace_id: 7,
|
|
126
|
+
user_id: 9,
|
|
127
|
+
role_sid: "member",
|
|
128
|
+
status: "pending",
|
|
129
|
+
created_at: "2026-03-09 00:26:35.710",
|
|
130
|
+
updated_at: "2026-03-09 00:26:35.710"
|
|
131
|
+
};
|
|
132
|
+
const refreshedRow = {
|
|
133
|
+
...existingRow,
|
|
134
|
+
role_sid: "owner",
|
|
135
|
+
status: "active",
|
|
136
|
+
updated_at: "2026-03-10 00:26:35.710"
|
|
137
|
+
};
|
|
138
|
+
const { knex, state } = createKnexStub({
|
|
139
|
+
rowById: new Map([["11", refreshedRow]]),
|
|
140
|
+
rowByComposite: new Map([["7:9", existingRow]])
|
|
141
|
+
});
|
|
142
|
+
const repository = createRepository(knex);
|
|
143
|
+
|
|
144
|
+
const membership = await repository.ensureOwnerMembership("7", "9");
|
|
145
|
+
|
|
146
|
+
assert.equal(state.updatePayload.role_sid, "owner");
|
|
147
|
+
assert.equal(state.updatePayload.status, "active");
|
|
148
|
+
assert.equal(typeof state.updatePayload.updated_at, "string");
|
|
149
|
+
assert.deepEqual(membership, {
|
|
150
|
+
id: "11",
|
|
151
|
+
workspaceId: "7",
|
|
152
|
+
userId: "9",
|
|
153
|
+
roleSid: "owner",
|
|
154
|
+
status: "active",
|
|
155
|
+
createdAt: toIsoString("2026-03-09 00:26:35.710"),
|
|
156
|
+
updatedAt: toIsoString(state.updatePayload.updated_at)
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("workspaceMembershipsRepository.upsertMembership creates normalized memberships through the runtime create path", async () => {
|
|
161
|
+
const createdRow = {
|
|
162
|
+
id: 1,
|
|
163
|
+
workspace_id: 7,
|
|
164
|
+
user_id: 9,
|
|
165
|
+
role_sid: "admin",
|
|
166
|
+
status: "active",
|
|
167
|
+
created_at: "2026-03-09 00:26:35.710",
|
|
168
|
+
updated_at: "2026-03-09 00:26:35.710"
|
|
169
|
+
};
|
|
170
|
+
const { knex, state } = createKnexStub({
|
|
171
|
+
rowById: new Map([["1", createdRow]]),
|
|
172
|
+
rowByComposite: new Map()
|
|
173
|
+
});
|
|
174
|
+
const repository = createRepository(knex);
|
|
175
|
+
|
|
176
|
+
await repository.upsertMembership("7", "9", {
|
|
177
|
+
roleSid: "ADMIN",
|
|
178
|
+
status: "ACTIVE"
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
assert.equal(state.insertPayload.workspace_id, "7");
|
|
182
|
+
assert.equal(state.insertPayload.user_id, "9");
|
|
183
|
+
assert.equal(state.insertPayload.role_sid, "admin");
|
|
184
|
+
assert.equal(state.insertPayload.status, "active");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("workspaceMembershipsRepository.listActiveByWorkspaceId keeps summary rows separate from the canonical membership resource", async () => {
|
|
188
|
+
const { knex } = createKnexStub({
|
|
189
|
+
memberSummaryRows: [
|
|
190
|
+
{
|
|
191
|
+
user_id: 9,
|
|
192
|
+
role_sid: "owner",
|
|
193
|
+
status: "active",
|
|
194
|
+
display_name: "Chiara",
|
|
195
|
+
email: "CHIARA@example.com"
|
|
196
|
+
}
|
|
197
|
+
]
|
|
198
|
+
});
|
|
199
|
+
const repository = createRepository(knex);
|
|
200
|
+
|
|
201
|
+
const members = await repository.listActiveByWorkspaceId("7");
|
|
202
|
+
|
|
203
|
+
assert.deepEqual(members, [
|
|
204
|
+
{
|
|
205
|
+
userId: "9",
|
|
206
|
+
roleSid: "owner",
|
|
207
|
+
status: "active",
|
|
208
|
+
displayName: "Chiara",
|
|
209
|
+
email: "chiara@example.com"
|
|
210
|
+
}
|
|
211
|
+
]);
|
|
212
|
+
});
|