@jskit-ai/workspaces-core 0.1.31 → 0.1.33

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.
Files changed (62) hide show
  1. package/package.descriptor.mjs +11 -22
  2. package/package.json +11 -9
  3. package/src/server/WorkspacesCoreServiceProvider.js +22 -2
  4. package/src/server/common/repositories/workspaceInvitesRepository.js +233 -78
  5. package/src/server/common/repositories/workspaceMembershipsRepository.js +177 -86
  6. package/src/server/common/repositories/workspacesRepository.js +179 -86
  7. package/src/server/common/services/workspaceContextService.js +28 -26
  8. package/src/server/common/validators/routeParamsValidator.js +36 -53
  9. package/src/server/registerWorkspaceCore.js +9 -10
  10. package/src/server/registerWorkspaceRepositories.js +7 -3
  11. package/src/server/support/workspaceServerScopeSupport.js +1 -1
  12. package/src/server/workspaceBootstrapContributor.js +5 -14
  13. package/src/server/workspaceDirectory/bootWorkspaceDirectoryRoutes.js +54 -27
  14. package/src/server/workspaceDirectory/workspaceDirectoryActions.js +30 -24
  15. package/src/server/workspaceMembers/bootWorkspaceMembers.js +70 -32
  16. package/src/server/workspaceMembers/workspaceMembersActions.js +61 -27
  17. package/src/server/workspaceMembers/workspaceMembersService.js +43 -7
  18. package/src/server/workspacePendingInvitations/bootWorkspacePendingInvitations.js +28 -13
  19. package/src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js +13 -15
  20. package/src/server/workspacePendingInvitations/workspacePendingInvitationsService.js +33 -10
  21. package/src/server/workspaceSettings/bootWorkspaceSettings.js +32 -13
  22. package/src/server/workspaceSettings/registerWorkspaceSettings.js +5 -1
  23. package/src/server/workspaceSettings/workspaceSettingsActions.js +18 -12
  24. package/src/server/workspaceSettings/workspaceSettingsRepository.js +104 -91
  25. package/src/server/workspaceSettings/workspaceSettingsService.js +5 -6
  26. package/src/shared/jsonApiTransports.js +79 -0
  27. package/src/shared/resources/workspaceInvitesResource.js +158 -0
  28. package/src/shared/resources/workspaceMembersResource.js +176 -311
  29. package/src/shared/resources/workspaceMembershipsResource.js +96 -0
  30. package/src/shared/resources/workspacePendingInvitationsResource.js +25 -72
  31. package/src/shared/resources/workspaceResource.js +113 -144
  32. package/src/shared/resources/workspaceRoleCatalogSchema.js +31 -0
  33. package/src/shared/resources/workspaceSettingsResource.js +276 -148
  34. package/test/repositoryContracts.test.js +16 -4
  35. package/test/resourcesCanonical.test.js +39 -16
  36. package/test/routeParamsValidator.test.js +37 -19
  37. package/test/usersRouteResources.test.js +27 -17
  38. package/test/workspaceActionContextContributor.test.js +1 -1
  39. package/test/workspaceInternalCrudResources.test.js +98 -0
  40. package/test/workspaceInvitesRepository.test.js +196 -148
  41. package/test/workspaceMembersResource.test.js +35 -0
  42. package/test/workspaceMembershipsRepository.test.js +155 -115
  43. package/test/workspacePendingInvitationsResource.test.js +18 -23
  44. package/test/workspacePendingInvitationsService.test.js +2 -1
  45. package/test/workspaceServerScopeSupport.test.js +77 -3
  46. package/test/workspaceService.test.js +26 -5
  47. package/test/workspaceSettingsActions.test.js +5 -7
  48. package/test/workspaceSettingsInternalResource.test.js +8 -0
  49. package/test/workspaceSettingsRepository.test.js +158 -123
  50. package/test/workspaceSettingsResource.test.js +51 -62
  51. package/test/workspaceSettingsService.test.js +0 -1
  52. package/test/workspacesRepository.test.js +318 -174
  53. package/test/workspacesRouteRequestInputValidator.test.js +25 -11
  54. package/src/server/common/resources/workspaceInvitesResource.js +0 -207
  55. package/src/server/common/resources/workspaceMembershipsResource.js +0 -154
  56. package/src/server/common/resources/workspacesResource.js +0 -170
  57. package/src/server/common/validators/authenticatedUserValidator.js +0 -43
  58. package/src/shared/resources/resolveGlobalArrayRegistry.js +0 -6
  59. package/src/shared/resources/workspaceSettingsFields.js +0 -65
  60. package/templates/packages/main/src/shared/resources/workspaceSettingsFields.js +0 -197
  61. package/test/settingsFieldRegistriesSingleton.test.js +0 -14
  62. 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 createWorkspacesKnexStub({
7
- rowById = new Map(),
8
- rowBySlug = new Map(),
9
- insertError = null,
10
- membershipRows = []
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
- insertPayload: null,
14
- updatePayload: null
71
+ postPayload: null,
72
+ patchPayload: null
15
73
  };
16
74
 
17
- function buildWorkspacesQuery(tableName) {
18
- const query = {
19
- tableName,
20
- selectedColumns: [],
21
- whereCriteria: [],
22
- orderByClauses: [],
23
- select(...columns) {
24
- this.selectedColumns = columns;
25
- return this;
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
- return query;
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
- function buildMembershipsQuery() {
75
- return {
76
- join() {
77
- return this;
78
- },
79
- where() {
80
- return this;
81
- },
82
- whereNull() {
83
- return this;
84
- },
85
- orderBy() {
86
- return this;
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
- select() {
89
- return Promise.resolve([...membershipRows]);
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
- function knex(tableName) {
95
- if (tableName === "workspaces" || tableName === "workspaces as w") {
96
- return buildWorkspacesQuery(tableName);
97
- }
98
- if (tableName === "workspace_memberships as wm") {
99
- return buildMembershipsQuery();
164
+ return { data: [] };
165
+ }
166
+ }
100
167
  }
101
- throw new Error(`Unexpected table ${tableName}`);
102
- }
103
-
104
- knex.transaction = async (work) => work(knex);
168
+ };
105
169
 
106
- return { knex, state };
170
+ return { api, state };
107
171
  }
108
172
 
109
- test("workspacesRepository.findById normalizes internal workspace fields via the internal resource", async () => {
110
- const { knex } = createWorkspacesKnexStub({
111
- rowById: new Map([
112
- [
113
- "7",
114
- {
115
- id: 7,
116
- slug: "tonymobily3",
117
- name: "TonyMobily3",
118
- owner_user_id: 9,
119
- is_personal: 1,
120
- avatar_url: "",
121
- created_at: "2026-03-09 00:26:35.710",
122
- updated_at: "2026-03-10 00:26:35.710",
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 null when no personal workspace exists", async () => {
146
- const { knex } = createWorkspacesKnexStub();
147
- const repository = createRepository(knex);
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("999");
237
+ const workspace = await repository.findPersonalByOwnerUserId("9");
150
238
 
151
- assert.equal(workspace, null);
239
+ assert.equal(workspace?.id, "7");
240
+ assert.equal(workspace?.slug, "first-workspace");
152
241
  });
153
242
 
154
- test("workspacesRepository.insert uses runtime normalization and timestamp columns", async () => {
155
- const insertedRow = {
156
- id: 1,
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
- createdAt: toIsoString("2026-03-09 00:26:35.710"),
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 workspace slug", async () => {
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
- owner_user_id: 9,
203
- is_personal: 0,
204
- avatar_url: "",
205
- created_at: "2026-03-09 00:26:35.710",
206
- updated_at: "2026-03-09 00:26:35.710",
207
- deleted_at: null
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 { knex } = createWorkspacesKnexStub({
210
- rowBySlug: new Map([["shared-workspace", existingRow]]),
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.listForUserId keeps membership-specific fields while normalizing workspace fields", async () => {
226
- const { knex } = createWorkspacesKnexStub({
227
- membershipRows: [
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
- owner_user_id: 9,
233
- is_personal: 1,
234
- avatar_url: "",
235
- created_at: "2026-03-09 00:26:35.710",
236
- updated_at: "2026-03-10 00:26:35.710",
237
- deleted_at: null,
238
- role_sid: "owner",
239
- membership_status: "active"
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 rows = await repository.listForUserId("9");
376
+ const workspaces = await repository.listForUserId("9");
246
377
 
247
- assert.deepEqual(rows, [
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 the shared transport normalizers they actually use", async () => {
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?.paramsValidator?.normalize, "function");
153
- assert.equal(typeof workspacePatch?.bodyValidator?.normalize, "function");
154
- assert.equal(typeof workspaceSettingsPatch?.bodyValidator?.normalize, "function");
155
- assert.equal(typeof workspaceMemberRole?.paramsValidator?.normalize, "function");
156
- assert.equal(typeof workspaceMemberRole?.bodyValidator?.normalize, "function");
157
- assert.equal(typeof workspaceMemberDelete?.paramsValidator?.normalize, "function");
158
- assert.equal(typeof workspaceInviteDelete?.paramsValidator?.normalize, "function");
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, { payload: { token: "token-1", decision: "accept" } });
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", patch: { lightPrimaryColor: "#0F6B54" } }
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
- patch: { name: "Acme", avatarUrl: "https://example.com/acme.png" }
484
+ name: "Acme",
485
+ avatarUrl: "https://example.com/acme.png"
472
486
  }
473
487
  });
474
488
  });