@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
@@ -1,27 +1,50 @@
1
- import { createCrudResourceRuntime } from "@jskit-ai/crud-core/server/resourceRuntime";
2
1
  import {
2
+ createWithTransaction,
3
3
  normalizeLowerText,
4
4
  normalizeRecordId,
5
5
  normalizeDbRecordId,
6
6
  normalizeText,
7
- isDuplicateEntryError
7
+ isDuplicateEntryError,
8
+ toIsoString
8
9
  } from "./repositoryUtils.js";
10
+ import {
11
+ createJsonApiInputRecord,
12
+ createJsonApiRelationship,
13
+ createJsonRestContext,
14
+ simplifyJsonApiDocument
15
+ } from "@jskit-ai/json-rest-api-core/server/jsonRestApiHost";
9
16
  import { OWNER_ROLE_ID } from "../../../shared/roles.js";
10
- import { workspaceMembershipsResource } from "../resources/workspaceMembershipsResource.js";
11
17
 
12
- const REPOSITORY_CONFIG = Object.freeze({
13
- context: "internal.repository.workspace-memberships"
14
- });
18
+ const RESOURCE_TYPE = "workspaceMemberships";
15
19
 
16
20
  function normalizeMembershipRecord(payload) {
17
21
  if (!payload) {
18
22
  return null;
19
23
  }
20
- return workspaceMembershipsResource.operations.view.outputValidator.normalize(payload);
24
+
25
+ return {
26
+ id: normalizeDbRecordId(payload.id, { fallback: null }),
27
+ workspaceId: normalizeDbRecordId(payload?.workspace?.id || payload?.workspaceId, { fallback: null }),
28
+ userId: normalizeDbRecordId(payload?.user?.id || payload?.userId, { fallback: null }),
29
+ roleSid: normalizeLowerText(payload.roleSid || "member") || "member",
30
+ status: normalizeLowerText(payload.status || "active") || "active",
31
+ createdAt: payload.createdAt ? toIsoString(payload.createdAt) : null,
32
+ updatedAt: payload.updatedAt ? toIsoString(payload.updatedAt) : null
33
+ };
21
34
  }
22
35
 
23
36
  function normalizeMembershipPatchPayload(payload = {}) {
24
- return workspaceMembershipsResource.operations.patch.bodyValidator.normalize(payload);
37
+ const source = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
38
+ const normalized = {};
39
+
40
+ if (Object.hasOwn(source, "roleSid")) {
41
+ normalized.roleSid = normalizeLowerText(source.roleSid);
42
+ }
43
+ if (Object.hasOwn(source, "status")) {
44
+ normalized.status = normalizeLowerText(source.status);
45
+ }
46
+
47
+ return normalized;
25
48
  }
26
49
 
27
50
  function normalizeMemberSummaryRow(row) {
@@ -38,12 +61,44 @@ function normalizeMemberSummaryRow(row) {
38
61
  };
39
62
  }
40
63
 
41
- function createRepository(knex) {
64
+ function createMembershipRelationships({ workspaceId = null, userId = null } = {}) {
65
+ const relationships = {};
66
+
67
+ if (workspaceId) {
68
+ relationships.workspace = createJsonApiRelationship("workspaces", workspaceId);
69
+ }
70
+ if (userId) {
71
+ relationships.user = createJsonApiRelationship("userProfiles", userId);
72
+ }
73
+
74
+ return relationships;
75
+ }
76
+
77
+ function createRepository({ api, knex } = {}) {
78
+ if (!api?.resources?.workspaceMemberships) {
79
+ throw new TypeError("workspaceMembershipsRepository requires json-rest-api workspaceMemberships resource.");
80
+ }
42
81
  if (typeof knex !== "function") {
43
82
  throw new TypeError("workspaceMembershipsRepository requires knex.");
44
83
  }
45
- const resourceRuntime = createCrudResourceRuntime(workspaceMembershipsResource, knex, REPOSITORY_CONFIG);
46
- const withTransaction = resourceRuntime.withTransaction;
84
+
85
+ const withTransaction = createWithTransaction(knex);
86
+
87
+ async function queryMemberships(filters = {}, options = {}, { includeUser = false } = {}) {
88
+ const result = await api.resources.workspaceMemberships.query(
89
+ {
90
+ queryParams: {
91
+ filters,
92
+ ...(includeUser ? { include: ["user"] } : {})
93
+ },
94
+ transaction: options?.trx || null,
95
+ simplified: false
96
+ },
97
+ createJsonRestContext(options?.context || null)
98
+ );
99
+
100
+ return Array.isArray(simplifyJsonApiDocument(result)) ? simplifyJsonApiDocument(result) : [];
101
+ }
47
102
 
48
103
  async function findByWorkspaceIdAndUserId(workspaceId, userId, options = {}) {
49
104
  const normalizedWorkspaceId = normalizeRecordId(workspaceId, { fallback: null });
@@ -52,11 +107,15 @@ function createRepository(knex) {
52
107
  return null;
53
108
  }
54
109
 
55
- const client = options?.trx || knex;
56
- const row = await client("workspace_memberships")
57
- .where({ workspace_id: normalizedWorkspaceId, user_id: normalizedUserId })
58
- .first();
59
- return normalizeMembershipRecord(row);
110
+ const rows = await queryMemberships(
111
+ {
112
+ workspace: normalizedWorkspaceId,
113
+ user: normalizedUserId
114
+ },
115
+ options
116
+ );
117
+
118
+ return normalizeMembershipRecord(rows[0] || null);
60
119
  }
61
120
 
62
121
  async function ensureOwnerMembership(workspaceId, userId, options = {}) {
@@ -66,40 +125,53 @@ function createRepository(knex) {
66
125
  throw new TypeError("workspaceMembershipsRepository.ensureOwnerMembership requires workspaceId and userId.");
67
126
  }
68
127
 
69
- const client = options?.trx || knex;
70
- const existing = await findByWorkspaceIdAndUserId(normalizedWorkspaceId, normalizedUserId, { trx: client });
128
+ const existing = await findByWorkspaceIdAndUserId(normalizedWorkspaceId, normalizedUserId, options);
71
129
  if (existing) {
72
130
  if (existing.roleSid !== OWNER_ROLE_ID || existing.status !== "active") {
73
- await resourceRuntime.updateById(
74
- existing.id,
131
+ await api.resources.workspaceMemberships.patch(
75
132
  {
76
- roleSid: OWNER_ROLE_ID,
77
- status: "active"
133
+ inputRecord: createJsonApiInputRecord(
134
+ RESOURCE_TYPE,
135
+ {
136
+ roleSid: OWNER_ROLE_ID,
137
+ status: "active",
138
+ updatedAt: new Date()
139
+ },
140
+ {
141
+ id: existing.id
142
+ }
143
+ ),
144
+ transaction: options?.trx || null,
145
+ simplified: false
78
146
  },
79
- {
80
- ...options,
81
- trx: client,
82
- include: "none",
83
- existingRecord: existing
84
- }
147
+ createJsonRestContext(options?.context || null)
85
148
  );
86
149
  }
87
- return findByWorkspaceIdAndUserId(normalizedWorkspaceId, normalizedUserId, { trx: client });
150
+ return findByWorkspaceIdAndUserId(normalizedWorkspaceId, normalizedUserId, options);
88
151
  }
89
152
 
90
153
  try {
91
- await resourceRuntime.create(
154
+ await api.resources.workspaceMemberships.post(
92
155
  {
93
- workspaceId: normalizedWorkspaceId,
94
- userId: normalizedUserId,
95
- roleSid: OWNER_ROLE_ID,
96
- status: "active"
156
+ inputRecord: createJsonApiInputRecord(
157
+ RESOURCE_TYPE,
158
+ {
159
+ roleSid: OWNER_ROLE_ID,
160
+ status: "active",
161
+ createdAt: new Date(),
162
+ updatedAt: new Date()
163
+ },
164
+ {
165
+ relationships: createMembershipRelationships({
166
+ workspaceId: normalizedWorkspaceId,
167
+ userId: normalizedUserId
168
+ })
169
+ }
170
+ ),
171
+ transaction: options?.trx || null,
172
+ simplified: false
97
173
  },
98
- {
99
- ...options,
100
- trx: client,
101
- include: "none"
102
- }
174
+ createJsonRestContext(options?.context || null)
103
175
  );
104
176
  } catch (error) {
105
177
  if (!isDuplicateEntryError(error)) {
@@ -107,7 +179,7 @@ function createRepository(knex) {
107
179
  }
108
180
  }
109
181
 
110
- return findByWorkspaceIdAndUserId(normalizedWorkspaceId, normalizedUserId, { trx: client });
182
+ return findByWorkspaceIdAndUserId(normalizedWorkspaceId, normalizedUserId, options);
111
183
  }
112
184
 
113
185
  async function upsertMembership(workspaceId, userId, patch = {}, options = {}) {
@@ -117,8 +189,7 @@ function createRepository(knex) {
117
189
  throw new TypeError("workspaceMembershipsRepository.upsertMembership requires workspaceId and userId.");
118
190
  }
119
191
 
120
- const client = options?.trx || knex;
121
- const existing = await findByWorkspaceIdAndUserId(normalizedWorkspaceId, normalizedUserId, { trx: client });
192
+ const existing = await findByWorkspaceIdAndUserId(normalizedWorkspaceId, normalizedUserId, options);
122
193
  const normalizedPatch = normalizeMembershipPatchPayload({
123
194
  roleSid: patch?.roleSid ?? existing?.roleSid ?? "member",
124
195
  status: patch?.status ?? existing?.status ?? "active"
@@ -128,42 +199,56 @@ function createRepository(knex) {
128
199
 
129
200
  if (!existing) {
130
201
  try {
131
- await resourceRuntime.create(
202
+ await api.resources.workspaceMemberships.post(
132
203
  {
133
- workspaceId: normalizedWorkspaceId,
134
- userId: normalizedUserId,
135
- roleSid,
136
- status
204
+ inputRecord: createJsonApiInputRecord(
205
+ RESOURCE_TYPE,
206
+ {
207
+ roleSid,
208
+ status,
209
+ createdAt: new Date(),
210
+ updatedAt: new Date()
211
+ },
212
+ {
213
+ relationships: createMembershipRelationships({
214
+ workspaceId: normalizedWorkspaceId,
215
+ userId: normalizedUserId
216
+ })
217
+ }
218
+ ),
219
+ transaction: options?.trx || null,
220
+ simplified: false
137
221
  },
138
- {
139
- ...options,
140
- trx: client,
141
- include: "none"
142
- }
222
+ createJsonRestContext(options?.context || null)
143
223
  );
144
224
  } catch (error) {
145
225
  if (!isDuplicateEntryError(error)) {
146
226
  throw error;
147
227
  }
148
228
  }
149
- return findByWorkspaceIdAndUserId(normalizedWorkspaceId, normalizedUserId, { trx: client });
229
+ return findByWorkspaceIdAndUserId(normalizedWorkspaceId, normalizedUserId, options);
150
230
  }
151
231
 
152
- await resourceRuntime.updateById(
153
- existing.id,
232
+ await api.resources.workspaceMemberships.patch(
154
233
  {
155
- roleSid,
156
- status
234
+ inputRecord: createJsonApiInputRecord(
235
+ RESOURCE_TYPE,
236
+ {
237
+ roleSid,
238
+ status,
239
+ updatedAt: new Date()
240
+ },
241
+ {
242
+ id: existing.id
243
+ }
244
+ ),
245
+ transaction: options?.trx || null,
246
+ simplified: false
157
247
  },
158
- {
159
- ...options,
160
- trx: client,
161
- include: "none",
162
- existingRecord: existing
163
- }
248
+ createJsonRestContext(options?.context || null)
164
249
  );
165
250
 
166
- return findByWorkspaceIdAndUserId(normalizedWorkspaceId, normalizedUserId, { trx: client });
251
+ return findByWorkspaceIdAndUserId(normalizedWorkspaceId, normalizedUserId, options);
167
252
  }
168
253
 
169
254
  async function listActiveByWorkspaceId(workspaceId, options = {}) {
@@ -172,20 +257,27 @@ function createRepository(knex) {
172
257
  return [];
173
258
  }
174
259
 
175
- const client = options?.trx || knex;
176
- const rows = await client("workspace_memberships as wm")
177
- .join("users as up", "up.id", "wm.user_id")
178
- .where({ "wm.workspace_id": normalizedWorkspaceId, "wm.status": "active" })
179
- .orderBy("up.display_name", "asc")
180
- .select([
181
- "wm.user_id",
182
- "wm.role_sid",
183
- "wm.status",
184
- "up.display_name",
185
- "up.email"
186
- ]);
187
-
188
- return rows.map(normalizeMemberSummaryRow).filter(Boolean);
260
+ const rows = await queryMemberships(
261
+ {
262
+ workspace: normalizedWorkspaceId,
263
+ status: "active"
264
+ },
265
+ options,
266
+ { includeUser: true }
267
+ );
268
+
269
+ const members = rows
270
+ .map((row) => normalizeMemberSummaryRow({
271
+ user_id: row?.user?.id,
272
+ role_sid: row?.roleSid,
273
+ status: row?.status,
274
+ display_name: row?.user?.displayName,
275
+ email: row?.user?.email
276
+ }))
277
+ .filter(Boolean);
278
+
279
+ members.sort((left, right) => String(left.displayName || "").localeCompare(String(right.displayName || "")));
280
+ return members;
189
281
  }
190
282
 
191
283
  async function listActiveWorkspaceIdsByUserId(userId, options = {}) {
@@ -194,17 +286,16 @@ function createRepository(knex) {
194
286
  return [];
195
287
  }
196
288
 
197
- const client = options?.trx || knex;
198
- const rows = await client("workspace_memberships")
199
- .where({
200
- user_id: normalizedUserId,
289
+ const rows = await queryMemberships(
290
+ {
291
+ user: normalizedUserId,
201
292
  status: "active"
202
- })
203
- .select("workspace_id")
204
- .orderBy("workspace_id", "asc");
293
+ },
294
+ options
295
+ );
205
296
 
206
297
  return rows
207
- .map((row) => normalizeDbRecordId(row.workspace_id, { fallback: null }))
298
+ .map((row) => normalizeDbRecordId(row?.workspace?.id || row?.workspaceId, { fallback: null }))
208
299
  .filter(Boolean);
209
300
  }
210
301