@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
|
@@ -3,129 +3,190 @@ import test from "node:test";
|
|
|
3
3
|
import { toIsoString } from "@jskit-ai/database-runtime/shared";
|
|
4
4
|
import { createRepository } from "../src/server/common/repositories/workspacesRepository.js";
|
|
5
5
|
|
|
6
|
-
function
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
function createKnexStub() {
|
|
7
|
+
return 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
|
+
|
|
16
|
+
function toWorkspaceResource(row = {}) {
|
|
17
|
+
return {
|
|
18
|
+
type: "workspaces",
|
|
19
|
+
id: String(row.id || ""),
|
|
20
|
+
attributes: {
|
|
21
|
+
slug: row.slug,
|
|
22
|
+
name: row.name,
|
|
23
|
+
isPersonal: row.isPersonal,
|
|
24
|
+
avatarUrl: row.avatarUrl,
|
|
25
|
+
createdAt: row.createdAt,
|
|
26
|
+
updatedAt: row.updatedAt,
|
|
27
|
+
deletedAt: row.deletedAt
|
|
28
|
+
},
|
|
29
|
+
relationships: {
|
|
30
|
+
owner: {
|
|
31
|
+
data: row.ownerUserId == null
|
|
32
|
+
? null
|
|
33
|
+
: {
|
|
34
|
+
type: "userProfiles",
|
|
35
|
+
id: String(row.ownerUserId)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function toWorkspaceMembershipResource(row = {}) {
|
|
43
|
+
return {
|
|
44
|
+
type: "workspaceMemberships",
|
|
45
|
+
id: String(row.id || ""),
|
|
46
|
+
attributes: {
|
|
47
|
+
roleSid: row.roleSid,
|
|
48
|
+
status: row.status,
|
|
49
|
+
createdAt: row.createdAt,
|
|
50
|
+
updatedAt: row.updatedAt
|
|
51
|
+
},
|
|
52
|
+
relationships: {
|
|
53
|
+
user: {
|
|
54
|
+
data: row?.user?.id == null ? null : { type: "userProfiles", id: String(row.user.id) }
|
|
55
|
+
},
|
|
56
|
+
workspace: {
|
|
57
|
+
data: row?.workspace?.id == null ? null : { type: "workspaces", id: String(row.workspace.id) }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function createWorkspacesApiStub({
|
|
64
|
+
rowsById = new Map(),
|
|
65
|
+
rowsBySlug = new Map(),
|
|
66
|
+
personalRowsByOwnerId = new Map(),
|
|
67
|
+
membershipRows = [],
|
|
68
|
+
insertError = null
|
|
11
69
|
} = {}) {
|
|
12
70
|
const state = {
|
|
13
|
-
|
|
14
|
-
|
|
71
|
+
postPayload: null,
|
|
72
|
+
patchPayload: null
|
|
15
73
|
};
|
|
16
74
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
},
|
|
27
|
-
where(criteria) {
|
|
28
|
-
this.whereCriteria.push(criteria);
|
|
29
|
-
return this;
|
|
30
|
-
},
|
|
31
|
-
orderBy(column, direction) {
|
|
32
|
-
this.orderByClauses.push({ column, direction });
|
|
33
|
-
return this;
|
|
34
|
-
},
|
|
35
|
-
async first() {
|
|
36
|
-
const criteria = Object.assign({}, ...this.whereCriteria);
|
|
37
|
-
if (Object.hasOwn(criteria, "w.id")) {
|
|
38
|
-
return rowById.get(String(criteria["w.id"])) || null;
|
|
39
|
-
}
|
|
40
|
-
if (Object.hasOwn(criteria, "id")) {
|
|
41
|
-
return rowById.get(String(criteria.id)) || null;
|
|
42
|
-
}
|
|
43
|
-
if (Object.hasOwn(criteria, "w.slug")) {
|
|
44
|
-
return rowBySlug.get(String(criteria["w.slug"])) || null;
|
|
45
|
-
}
|
|
46
|
-
if (Object.hasOwn(criteria, "w.owner_user_id") && Object.hasOwn(criteria, "w.is_personal")) {
|
|
47
|
-
for (const row of rowById.values()) {
|
|
48
|
-
if (
|
|
49
|
-
String(row.owner_user_id) === String(criteria["w.owner_user_id"]) &&
|
|
50
|
-
Number(row.is_personal) === Number(criteria["w.is_personal"])
|
|
51
|
-
) {
|
|
52
|
-
return row;
|
|
53
|
-
}
|
|
75
|
+
const api = {
|
|
76
|
+
resources: {
|
|
77
|
+
workspaces: {
|
|
78
|
+
async query({ queryParams }) {
|
|
79
|
+
const filters = queryParams?.filters || {};
|
|
80
|
+
|
|
81
|
+
if (Object.hasOwn(filters, "id")) {
|
|
82
|
+
const row = rowsById.get(String(filters.id)) || null;
|
|
83
|
+
return { data: row ? [toWorkspaceResource(row)] : [] };
|
|
54
84
|
}
|
|
55
|
-
}
|
|
56
|
-
return null;
|
|
57
|
-
},
|
|
58
|
-
async insert(payload) {
|
|
59
|
-
state.insertPayload = payload;
|
|
60
|
-
if (insertError) {
|
|
61
|
-
throw insertError;
|
|
62
|
-
}
|
|
63
|
-
return [1];
|
|
64
|
-
},
|
|
65
|
-
async update(payload) {
|
|
66
|
-
state.updatePayload = payload;
|
|
67
|
-
return 1;
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
85
|
|
|
71
|
-
|
|
72
|
-
|
|
86
|
+
if (Object.hasOwn(filters, "slug")) {
|
|
87
|
+
const row = rowsBySlug.get(String(filters.slug)) || null;
|
|
88
|
+
return { data: row ? [toWorkspaceResource(row)] : [] };
|
|
89
|
+
}
|
|
73
90
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
91
|
+
if (Object.hasOwn(filters, "owner") && Object.hasOwn(filters, "isPersonal")) {
|
|
92
|
+
const rows = personalRowsByOwnerId.get(String(filters.owner)) || [];
|
|
93
|
+
return { data: rows.map((row) => toWorkspaceResource(row)) };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { data: [] };
|
|
97
|
+
},
|
|
98
|
+
async post(payload) {
|
|
99
|
+
assert.equal(payload?.simplified, false);
|
|
100
|
+
const inputRecord = payload?.inputRecord?.data || {};
|
|
101
|
+
state.postPayload = inputRecord;
|
|
102
|
+
if (insertError) {
|
|
103
|
+
throw insertError;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const row = {
|
|
107
|
+
id: "1",
|
|
108
|
+
slug: String(inputRecord.attributes?.slug || ""),
|
|
109
|
+
name: String(inputRecord.attributes?.name || ""),
|
|
110
|
+
ownerUserId: String(inputRecord.relationships?.owner?.data?.id || ""),
|
|
111
|
+
isPersonal: Boolean(inputRecord.attributes?.isPersonal),
|
|
112
|
+
avatarUrl: String(inputRecord.attributes?.avatarUrl || ""),
|
|
113
|
+
createdAt: inputRecord.attributes?.createdAt,
|
|
114
|
+
updatedAt: inputRecord.attributes?.updatedAt,
|
|
115
|
+
deletedAt: null
|
|
116
|
+
};
|
|
117
|
+
rowsById.set(row.id, row);
|
|
118
|
+
if (row.slug) {
|
|
119
|
+
rowsBySlug.set(row.slug, row);
|
|
120
|
+
}
|
|
121
|
+
return { data: toWorkspaceResource(row) };
|
|
122
|
+
},
|
|
123
|
+
async patch(payload) {
|
|
124
|
+
assert.equal(payload?.simplified, false);
|
|
125
|
+
const inputRecord = payload?.inputRecord?.data || {};
|
|
126
|
+
state.patchPayload = inputRecord;
|
|
127
|
+
const existing = rowsById.get(String(inputRecord.id)) || {
|
|
128
|
+
id: String(inputRecord.id)
|
|
129
|
+
};
|
|
130
|
+
const updated = {
|
|
131
|
+
...existing,
|
|
132
|
+
...(inputRecord.attributes || {}),
|
|
133
|
+
...(inputRecord.relationships?.owner?.data?.id
|
|
134
|
+
? { ownerUserId: String(inputRecord.relationships.owner.data.id) }
|
|
135
|
+
: {}),
|
|
136
|
+
id: String(inputRecord.id)
|
|
137
|
+
};
|
|
138
|
+
rowsById.set(updated.id, updated);
|
|
139
|
+
if (updated.slug) {
|
|
140
|
+
rowsBySlug.set(String(updated.slug), updated);
|
|
141
|
+
}
|
|
142
|
+
return { data: toWorkspaceResource(updated) };
|
|
143
|
+
}
|
|
87
144
|
},
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
145
|
+
workspaceMemberships: {
|
|
146
|
+
async query({ queryParams }) {
|
|
147
|
+
const filters = queryParams?.filters || {};
|
|
148
|
+
const includeWorkspace = Array.isArray(queryParams?.include) && queryParams.include.includes("workspace");
|
|
149
|
+
if (Object.hasOwn(filters, "user") && Object.hasOwn(filters, "status")) {
|
|
150
|
+
const rows = membershipRows.filter((row) => (
|
|
151
|
+
String(row?.user?.id || "") === String(filters.user) &&
|
|
152
|
+
String(row?.status || "") === String(filters.status)
|
|
153
|
+
));
|
|
154
|
+
return {
|
|
155
|
+
data: rows.map((row) => toWorkspaceMembershipResource(row)),
|
|
156
|
+
included: includeWorkspace
|
|
157
|
+
? rows
|
|
158
|
+
.filter((row) => row?.workspace?.id != null)
|
|
159
|
+
.map((row) => toWorkspaceResource(row.workspace))
|
|
160
|
+
: []
|
|
161
|
+
};
|
|
162
|
+
}
|
|
93
163
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
if (tableName === "workspace_memberships as wm") {
|
|
99
|
-
return buildMembershipsQuery();
|
|
164
|
+
return { data: [] };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
100
167
|
}
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
knex.transaction = async (work) => work(knex);
|
|
168
|
+
};
|
|
105
169
|
|
|
106
|
-
return {
|
|
170
|
+
return { api, state };
|
|
107
171
|
}
|
|
108
172
|
|
|
109
|
-
test("workspacesRepository.findById
|
|
110
|
-
const {
|
|
111
|
-
|
|
112
|
-
[
|
|
113
|
-
"7",
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
deleted_at: null
|
|
124
|
-
}
|
|
125
|
-
]
|
|
173
|
+
test("workspacesRepository.findById reads a canonical workspace row through json-rest-api", async () => {
|
|
174
|
+
const { api } = createWorkspacesApiStub({
|
|
175
|
+
rowsById: new Map([
|
|
176
|
+
["7", {
|
|
177
|
+
id: "7",
|
|
178
|
+
slug: "tonymobily3",
|
|
179
|
+
name: "TonyMobily3",
|
|
180
|
+
ownerUserId: "9",
|
|
181
|
+
isPersonal: true,
|
|
182
|
+
avatarUrl: "",
|
|
183
|
+
createdAt: "2026-03-09 00:26:35.710",
|
|
184
|
+
updatedAt: "2026-03-10 00:26:35.710",
|
|
185
|
+
deletedAt: null
|
|
186
|
+
}]
|
|
126
187
|
])
|
|
127
188
|
});
|
|
128
|
-
const repository = createRepository(knex);
|
|
189
|
+
const repository = createRepository({ api, knex: createKnexStub() });
|
|
129
190
|
|
|
130
191
|
const workspace = await repository.findById("7");
|
|
131
192
|
|
|
@@ -142,75 +203,83 @@ test("workspacesRepository.findById normalizes internal workspace fields via the
|
|
|
142
203
|
});
|
|
143
204
|
});
|
|
144
205
|
|
|
145
|
-
test("workspacesRepository.findPersonalByOwnerUserId returns
|
|
146
|
-
const {
|
|
147
|
-
|
|
206
|
+
test("workspacesRepository.findPersonalByOwnerUserId returns the first personal workspace by canonical id order", async () => {
|
|
207
|
+
const { api } = createWorkspacesApiStub({
|
|
208
|
+
personalRowsByOwnerId: new Map([
|
|
209
|
+
["9", [
|
|
210
|
+
{
|
|
211
|
+
id: "12",
|
|
212
|
+
slug: "later-workspace",
|
|
213
|
+
name: "Later Workspace",
|
|
214
|
+
ownerUserId: "9",
|
|
215
|
+
isPersonal: true,
|
|
216
|
+
avatarUrl: "",
|
|
217
|
+
createdAt: "2026-03-09 00:26:35.710",
|
|
218
|
+
updatedAt: "2026-03-09 00:26:35.710",
|
|
219
|
+
deletedAt: null
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
id: "7",
|
|
223
|
+
slug: "first-workspace",
|
|
224
|
+
name: "First Workspace",
|
|
225
|
+
ownerUserId: "9",
|
|
226
|
+
isPersonal: true,
|
|
227
|
+
avatarUrl: "",
|
|
228
|
+
createdAt: "2026-03-08 00:26:35.710",
|
|
229
|
+
updatedAt: "2026-03-08 00:26:35.710",
|
|
230
|
+
deletedAt: null
|
|
231
|
+
}
|
|
232
|
+
]]
|
|
233
|
+
])
|
|
234
|
+
});
|
|
235
|
+
const repository = createRepository({ api, knex: createKnexStub() });
|
|
148
236
|
|
|
149
|
-
const workspace = await repository.findPersonalByOwnerUserId("
|
|
237
|
+
const workspace = await repository.findPersonalByOwnerUserId("9");
|
|
150
238
|
|
|
151
|
-
assert.equal(workspace,
|
|
239
|
+
assert.equal(workspace?.id, "7");
|
|
240
|
+
assert.equal(workspace?.slug, "first-workspace");
|
|
152
241
|
});
|
|
153
242
|
|
|
154
|
-
test("workspacesRepository.insert
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
slug: "tonymobily3",
|
|
158
|
-
name: "TonyMobily3",
|
|
159
|
-
owner_user_id: 9,
|
|
160
|
-
is_personal: 0,
|
|
161
|
-
avatar_url: "",
|
|
162
|
-
created_at: "2026-03-09 00:26:35.710",
|
|
163
|
-
updated_at: "2026-03-09 00:26:35.710",
|
|
164
|
-
deleted_at: null
|
|
165
|
-
};
|
|
166
|
-
const { knex, state } = createWorkspacesKnexStub({
|
|
167
|
-
rowById: new Map([["1", insertedRow]])
|
|
168
|
-
});
|
|
169
|
-
const repository = createRepository(knex);
|
|
243
|
+
test("workspacesRepository.insert writes canonical fields through json-rest-api", async () => {
|
|
244
|
+
const { api, state } = createWorkspacesApiStub();
|
|
245
|
+
const repository = createRepository({ api, knex: createKnexStub() });
|
|
170
246
|
|
|
171
247
|
const inserted = await repository.insert({
|
|
172
248
|
slug: "TonyMobily3",
|
|
173
249
|
name: "TonyMobily3",
|
|
174
|
-
ownerUserId: "9"
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
assert.equal(state.insertPayload.slug, "tonymobily3");
|
|
178
|
-
assert.equal(state.insertPayload.name, "TonyMobily3");
|
|
179
|
-
assert.equal(state.insertPayload.owner_user_id, "9");
|
|
180
|
-
assert.equal(state.insertPayload.is_personal, false);
|
|
181
|
-
assert.equal(state.insertPayload.avatar_url, "");
|
|
182
|
-
assert.equal(typeof state.insertPayload.created_at, "string");
|
|
183
|
-
assert.equal(typeof state.insertPayload.updated_at, "string");
|
|
184
|
-
assert.deepEqual(inserted, {
|
|
185
|
-
id: "1",
|
|
186
|
-
slug: "tonymobily3",
|
|
187
|
-
name: "TonyMobily3",
|
|
188
250
|
ownerUserId: "9",
|
|
189
|
-
isPersonal: false,
|
|
190
251
|
avatarUrl: "",
|
|
191
|
-
|
|
192
|
-
updatedAt: toIsoString("2026-03-09 00:26:35.710"),
|
|
193
|
-
deletedAt: null
|
|
252
|
+
isPersonal: false
|
|
194
253
|
});
|
|
254
|
+
|
|
255
|
+
assert.equal(state.postPayload.relationships?.owner?.data?.id, "9");
|
|
256
|
+
assert.equal(state.postPayload.attributes?.slug, "TonyMobily3");
|
|
257
|
+
assert.equal(state.postPayload.attributes?.name, "TonyMobily3");
|
|
258
|
+
assert.equal(state.postPayload.attributes?.isPersonal, false);
|
|
259
|
+
assert.equal(state.postPayload.attributes?.avatarUrl, "");
|
|
260
|
+
assert.equal(typeof state.postPayload.attributes?.createdAt, "object");
|
|
261
|
+
assert.equal(typeof state.postPayload.attributes?.updatedAt, "object");
|
|
262
|
+
assert.equal(inserted.id, "1");
|
|
263
|
+
assert.equal(inserted.ownerUserId, "9");
|
|
195
264
|
});
|
|
196
265
|
|
|
197
|
-
test("workspacesRepository.insert falls back to slug lookup on duplicate
|
|
266
|
+
test("workspacesRepository.insert falls back to slug lookup on duplicate slug", async () => {
|
|
198
267
|
const existingRow = {
|
|
199
|
-
id: 12,
|
|
268
|
+
id: "12",
|
|
200
269
|
slug: "shared-workspace",
|
|
201
270
|
name: "Shared Workspace",
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
271
|
+
ownerUserId: "9",
|
|
272
|
+
isPersonal: false,
|
|
273
|
+
avatarUrl: "",
|
|
274
|
+
createdAt: "2026-03-09 00:26:35.710",
|
|
275
|
+
updatedAt: "2026-03-09 00:26:35.710",
|
|
276
|
+
deletedAt: null
|
|
208
277
|
};
|
|
209
|
-
const {
|
|
210
|
-
|
|
278
|
+
const { api } = createWorkspacesApiStub({
|
|
279
|
+
rowsBySlug: new Map([["shared-workspace", existingRow]]),
|
|
211
280
|
insertError: { code: "ER_DUP_ENTRY" }
|
|
212
281
|
});
|
|
213
|
-
const repository = createRepository(knex);
|
|
282
|
+
const repository = createRepository({ api, knex: createKnexStub() });
|
|
214
283
|
|
|
215
284
|
const inserted = await repository.insert({
|
|
216
285
|
slug: "shared-workspace",
|
|
@@ -222,29 +291,91 @@ test("workspacesRepository.insert falls back to slug lookup on duplicate workspa
|
|
|
222
291
|
assert.equal(inserted?.slug, "shared-workspace");
|
|
223
292
|
});
|
|
224
293
|
|
|
225
|
-
test("workspacesRepository.
|
|
226
|
-
const {
|
|
227
|
-
|
|
228
|
-
{
|
|
229
|
-
id: 7,
|
|
294
|
+
test("workspacesRepository.updateById patches canonical fields and updatedAt", async () => {
|
|
295
|
+
const { api, state } = createWorkspacesApiStub({
|
|
296
|
+
rowsById: new Map([
|
|
297
|
+
["7", {
|
|
298
|
+
id: "7",
|
|
230
299
|
slug: "tonymobily3",
|
|
231
300
|
name: "TonyMobily3",
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
301
|
+
ownerUserId: "9",
|
|
302
|
+
isPersonal: false,
|
|
303
|
+
avatarUrl: "",
|
|
304
|
+
createdAt: "2026-03-09 00:26:35.710",
|
|
305
|
+
updatedAt: "2026-03-09 00:26:35.710",
|
|
306
|
+
deletedAt: null
|
|
307
|
+
}]
|
|
308
|
+
])
|
|
309
|
+
});
|
|
310
|
+
const repository = createRepository({ api, knex: createKnexStub() });
|
|
311
|
+
|
|
312
|
+
await repository.updateById("7", {
|
|
313
|
+
name: "TonyMobily 4"
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
assert.equal(state.patchPayload.id, "7");
|
|
317
|
+
assert.equal(state.patchPayload.attributes?.name, "TonyMobily 4");
|
|
318
|
+
assert.equal(typeof state.patchPayload.attributes?.updatedAt, "object");
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test("workspacesRepository.listForUserId keeps membership fields outside the canonical workspace row", async () => {
|
|
322
|
+
const { api } = createWorkspacesApiStub({
|
|
323
|
+
membershipRows: [
|
|
324
|
+
{
|
|
325
|
+
user: { id: "9" },
|
|
326
|
+
roleSid: "owner",
|
|
327
|
+
status: "active",
|
|
328
|
+
workspace: {
|
|
329
|
+
id: "7",
|
|
330
|
+
slug: "tonymobily3",
|
|
331
|
+
name: "TonyMobily3",
|
|
332
|
+
ownerUserId: "9",
|
|
333
|
+
isPersonal: true,
|
|
334
|
+
avatarUrl: "",
|
|
335
|
+
createdAt: "2026-03-09 00:26:35.710",
|
|
336
|
+
updatedAt: "2026-03-10 00:26:35.710",
|
|
337
|
+
deletedAt: null
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
user: { id: "9" },
|
|
342
|
+
roleSid: "member",
|
|
343
|
+
status: "active",
|
|
344
|
+
workspace: {
|
|
345
|
+
id: "8",
|
|
346
|
+
slug: "team-space",
|
|
347
|
+
name: "Team Space",
|
|
348
|
+
ownerUserId: "10",
|
|
349
|
+
isPersonal: false,
|
|
350
|
+
avatarUrl: "",
|
|
351
|
+
createdAt: "2026-03-09 00:26:35.710",
|
|
352
|
+
updatedAt: "2026-03-10 00:26:35.710",
|
|
353
|
+
deletedAt: null
|
|
354
|
+
}
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
user: { id: "9" },
|
|
358
|
+
roleSid: "member",
|
|
359
|
+
status: "active",
|
|
360
|
+
workspace: {
|
|
361
|
+
id: "9",
|
|
362
|
+
slug: "deleted-space",
|
|
363
|
+
name: "Deleted Space",
|
|
364
|
+
ownerUserId: "10",
|
|
365
|
+
isPersonal: false,
|
|
366
|
+
avatarUrl: "",
|
|
367
|
+
createdAt: "2026-03-09 00:26:35.710",
|
|
368
|
+
updatedAt: "2026-03-10 00:26:35.710",
|
|
369
|
+
deletedAt: "2026-03-11 00:26:35.710"
|
|
370
|
+
}
|
|
240
371
|
}
|
|
241
372
|
]
|
|
242
373
|
});
|
|
243
|
-
const repository = createRepository(knex);
|
|
374
|
+
const repository = createRepository({ api, knex: createKnexStub() });
|
|
244
375
|
|
|
245
|
-
const
|
|
376
|
+
const workspaces = await repository.listForUserId("9");
|
|
246
377
|
|
|
247
|
-
assert.deepEqual(
|
|
378
|
+
assert.deepEqual(workspaces, [
|
|
248
379
|
{
|
|
249
380
|
id: "7",
|
|
250
381
|
slug: "tonymobily3",
|
|
@@ -257,6 +388,19 @@ test("workspacesRepository.listForUserId keeps membership-specific fields while
|
|
|
257
388
|
deletedAt: null,
|
|
258
389
|
roleSid: "owner",
|
|
259
390
|
membershipStatus: "active"
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
id: "8",
|
|
394
|
+
slug: "team-space",
|
|
395
|
+
name: "Team Space",
|
|
396
|
+
ownerUserId: "10",
|
|
397
|
+
isPersonal: false,
|
|
398
|
+
avatarUrl: "",
|
|
399
|
+
createdAt: toIsoString("2026-03-09 00:26:35.710"),
|
|
400
|
+
updatedAt: toIsoString("2026-03-10 00:26:35.710"),
|
|
401
|
+
deletedAt: null,
|
|
402
|
+
roleSid: "member",
|
|
403
|
+
membershipStatus: "active"
|
|
260
404
|
}
|
|
261
405
|
]);
|
|
262
406
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import { UsersCoreServiceProvider } from "../../users-core/src/server/UsersCoreServiceProvider.js";
|
|
4
|
+
import { INTERNAL_JSON_REST_API } from "../../json-rest-api-core/src/server/jsonRestApiHost.js";
|
|
4
5
|
import { resolveTenancyProfile } from "../src/shared/tenancyProfile.js";
|
|
5
6
|
import { WorkspacesCoreServiceProvider } from "../src/server/WorkspacesCoreServiceProvider.js";
|
|
6
7
|
|
|
@@ -36,6 +37,13 @@ async function registerRoutes({
|
|
|
36
37
|
workspaceInvitationsEnabled = true,
|
|
37
38
|
workspaceSelfCreateEnabled = true
|
|
38
39
|
} = {}) {
|
|
40
|
+
const internalApi = {
|
|
41
|
+
resources: {},
|
|
42
|
+
async addResource(scopeName) {
|
|
43
|
+
this.resources[scopeName] = {};
|
|
44
|
+
return this.resources[scopeName];
|
|
45
|
+
}
|
|
46
|
+
};
|
|
39
47
|
const registeredRoutes = [];
|
|
40
48
|
const router = {
|
|
41
49
|
register(method, path, route, handler) {
|
|
@@ -51,6 +59,7 @@ async function registerRoutes({
|
|
|
51
59
|
const bindings = new Map([
|
|
52
60
|
["jskit.http.router", router],
|
|
53
61
|
["authService", authService],
|
|
62
|
+
[INTERNAL_JSON_REST_API, internalApi],
|
|
54
63
|
[
|
|
55
64
|
"users.accountProfile.service",
|
|
56
65
|
{
|
|
@@ -77,6 +86,10 @@ async function registerRoutes({
|
|
|
77
86
|
has(token) {
|
|
78
87
|
return bindings.has(token);
|
|
79
88
|
},
|
|
89
|
+
instance(token, value) {
|
|
90
|
+
bindings.set(token, value);
|
|
91
|
+
return this;
|
|
92
|
+
},
|
|
80
93
|
make(token) {
|
|
81
94
|
if (!bindings.has(token)) {
|
|
82
95
|
throw new Error(`Missing test binding for token: ${String(token)}`);
|
|
@@ -121,7 +134,7 @@ function createActionRequest({ input = {}, executeAction, file = null }) {
|
|
|
121
134
|
};
|
|
122
135
|
}
|
|
123
136
|
|
|
124
|
-
test("workspace and settings routes attach only
|
|
137
|
+
test("workspace and settings routes attach only shared schema definitions on raw route contracts", async () => {
|
|
125
138
|
const routes = await registerRoutes();
|
|
126
139
|
|
|
127
140
|
const workspaceSettings = findRoute(routes, {
|
|
@@ -149,13 +162,13 @@ test("workspace and settings routes attach only the shared transport normalizers
|
|
|
149
162
|
path: "/api/w/:workspaceSlug/invites/:inviteId"
|
|
150
163
|
});
|
|
151
164
|
|
|
152
|
-
assert.equal(typeof workspaceSettings?.
|
|
153
|
-
assert.equal(typeof workspacePatch?.
|
|
154
|
-
assert.equal(typeof workspaceSettingsPatch?.
|
|
155
|
-
assert.equal(typeof workspaceMemberRole?.
|
|
156
|
-
assert.equal(typeof workspaceMemberRole?.
|
|
157
|
-
assert.equal(typeof workspaceMemberDelete?.
|
|
158
|
-
assert.equal(typeof workspaceInviteDelete?.
|
|
165
|
+
assert.equal(typeof workspaceSettings?.params?.schema, "object");
|
|
166
|
+
assert.equal(typeof workspacePatch?.body?.schema, "object");
|
|
167
|
+
assert.equal(typeof workspaceSettingsPatch?.body?.schema, "object");
|
|
168
|
+
assert.equal(typeof workspaceMemberRole?.params?.schema, "object");
|
|
169
|
+
assert.equal(typeof workspaceMemberRole?.body?.schema, "object");
|
|
170
|
+
assert.equal(typeof workspaceMemberDelete?.params?.schema, "object");
|
|
171
|
+
assert.equal(typeof workspaceInviteDelete?.params?.schema, "object");
|
|
159
172
|
});
|
|
160
173
|
|
|
161
174
|
test("workspace core/settings routes mount one canonical workspace endpoint", async () => {
|
|
@@ -405,7 +418,7 @@ test("workspace invite and member handlers build action input from request.input
|
|
|
405
418
|
actionId: "workspace.workspaces.create",
|
|
406
419
|
input: { name: "Operations", slug: "operations" }
|
|
407
420
|
});
|
|
408
|
-
assert.deepEqual(calls[1].input, {
|
|
421
|
+
assert.deepEqual(calls[1].input, { token: "token-1", decision: "accept" });
|
|
409
422
|
assert.deepEqual(calls[2].input, { workspaceSlug: "acme", memberUserId: "12", roleSid: "admin" });
|
|
410
423
|
assert.deepEqual(calls[3].input, { workspaceSlug: "acme", email: "user@example.com", roleSid: "member" });
|
|
411
424
|
assert.deepEqual(calls[4].input, { workspaceSlug: "acme", memberUserId: "44" });
|
|
@@ -437,7 +450,7 @@ test("workspace settings route handlers build action input from request.input",
|
|
|
437
450
|
|
|
438
451
|
assert.deepEqual(calls[0], {
|
|
439
452
|
actionId: "workspace.settings.update",
|
|
440
|
-
input: { workspaceSlug: "acme",
|
|
453
|
+
input: { workspaceSlug: "acme", lightPrimaryColor: "#0F6B54" }
|
|
441
454
|
});
|
|
442
455
|
});
|
|
443
456
|
|
|
@@ -468,7 +481,8 @@ test("workspace route handlers build action input from request.input", async ()
|
|
|
468
481
|
actionId: "workspace.workspaces.update",
|
|
469
482
|
input: {
|
|
470
483
|
workspaceSlug: "acme",
|
|
471
|
-
|
|
484
|
+
name: "Acme",
|
|
485
|
+
avatarUrl: "https://example.com/acme.png"
|
|
472
486
|
}
|
|
473
487
|
});
|
|
474
488
|
});
|