@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,62 +1,72 @@
1
- import { createCrudResourceRuntime } from "@jskit-ai/crud-core/server/resourceRuntime";
2
1
  import {
2
+ createWithTransaction,
3
3
  normalizeRecordId,
4
+ isDuplicateEntryError,
4
5
  normalizeText,
5
- normalizeLowerText,
6
- isDuplicateEntryError
6
+ toIsoString
7
7
  } from "./repositoryUtils.js";
8
- import { workspacesResource } from "../resources/workspacesResource.js";
8
+ import {
9
+ createJsonApiInputRecord,
10
+ createJsonApiRelationship,
11
+ createJsonRestContext,
12
+ simplifyJsonApiDocument
13
+ } from "@jskit-ai/json-rest-api-core/server/jsonRestApiHost";
9
14
 
10
- const REPOSITORY_CONFIG = Object.freeze({
11
- context: "internal.repository.workspaces"
12
- });
15
+ const RESOURCE_TYPE = "workspaces";
13
16
 
14
- function normalizeWorkspaceRecord(payload) {
15
- if (!payload) {
17
+ function normalizeWorkspaceRecord(payload = null) {
18
+ if (!payload || typeof payload !== "object") {
16
19
  return null;
17
20
  }
18
- return workspacesResource.operations.view.outputValidator.normalize(payload);
19
- }
20
21
 
21
- function normalizeCreatePayload(payload = {}) {
22
- return workspacesResource.operations.create.bodyValidator.normalize(payload);
22
+ return {
23
+ id: normalizeRecordId(payload.id, { fallback: null }),
24
+ slug: normalizeText(payload.slug),
25
+ name: normalizeText(payload.name),
26
+ ownerUserId: normalizeRecordId(payload.ownerUserId || payload?.owner?.id, { fallback: null }),
27
+ isPersonal: payload.isPersonal === true,
28
+ avatarUrl: normalizeText(payload.avatarUrl),
29
+ createdAt: payload.createdAt ? toIsoString(payload.createdAt) : null,
30
+ updatedAt: payload.updatedAt ? toIsoString(payload.updatedAt) : null,
31
+ deletedAt: payload.deletedAt ? toIsoString(payload.deletedAt) : null
32
+ };
23
33
  }
24
34
 
25
- function normalizeMembershipWorkspaceRow(row) {
26
- if (!row) {
27
- return null;
35
+ function createWorkspaceRelationships(source = {}) {
36
+ const relationships = {};
37
+ const ownerUserId = normalizeRecordId(source.ownerUserId, { fallback: null });
38
+
39
+ if (ownerUserId) {
40
+ relationships.owner = createJsonApiRelationship("userProfiles", ownerUserId);
28
41
  }
29
42
 
30
- return {
31
- ...normalizeWorkspaceRecord(row),
32
- roleSid: normalizeLowerText(row.role_sid || "member"),
33
- membershipStatus: normalizeLowerText(row.membership_status || "active") || "active"
34
- };
43
+ return relationships;
35
44
  }
36
45
 
37
- function createRepository(knex) {
46
+ function createRepository({ api, knex } = {}) {
47
+ if (!api?.resources?.workspaces || !api?.resources?.workspaceMemberships) {
48
+ throw new TypeError("workspacesRepository requires json-rest-api workspaces and workspaceMemberships resources.");
49
+ }
38
50
  if (typeof knex !== "function") {
39
51
  throw new TypeError("workspacesRepository requires knex.");
40
52
  }
41
- const resourceRuntime = createCrudResourceRuntime(workspacesResource, knex, REPOSITORY_CONFIG);
42
- const withTransaction = resourceRuntime.withTransaction;
43
-
44
- function workspaceSelectColumns({ includeMembership = false } = {}) {
45
- const columns = [
46
- "w.id",
47
- "w.slug",
48
- "w.name",
49
- "w.owner_user_id",
50
- "w.is_personal",
51
- "w.avatar_url",
52
- "w.created_at",
53
- "w.updated_at",
54
- "w.deleted_at"
55
- ];
56
- if (includeMembership) {
57
- columns.push("wm.role_sid", "wm.status as membership_status");
58
- }
59
- return columns;
53
+
54
+ const withTransaction = createWithTransaction(knex);
55
+
56
+ async function queryFirst(filters = {}, options = {}) {
57
+ const result = await api.resources.workspaces.query(
58
+ {
59
+ queryParams: {
60
+ filters
61
+ },
62
+ transaction: options?.trx || null,
63
+ simplified: false
64
+ },
65
+ createJsonRestContext(options?.context || null)
66
+ );
67
+
68
+ const rows = simplifyJsonApiDocument(result);
69
+ return normalizeWorkspaceRecord(Array.isArray(rows) ? rows[0] || null : null);
60
70
  }
61
71
 
62
72
  async function findById(workspaceId, options = {}) {
@@ -65,24 +75,16 @@ function createRepository(knex) {
65
75
  return null;
66
76
  }
67
77
 
68
- return resourceRuntime.findById(normalizedWorkspaceId, {
69
- ...options,
70
- include: "none"
71
- });
78
+ return queryFirst({ id: normalizedWorkspaceId }, options);
72
79
  }
73
80
 
74
81
  async function findBySlug(slug, options = {}) {
75
- const client = options?.trx || knex;
76
- const normalizedSlug = normalizeLowerText(slug);
82
+ const normalizedSlug = typeof slug === "string" ? slug.trim().toLowerCase() : "";
77
83
  if (!normalizedSlug) {
78
84
  return null;
79
85
  }
80
86
 
81
- const row = await client("workspaces as w")
82
- .where({ "w.slug": normalizedSlug })
83
- .select(workspaceSelectColumns())
84
- .first();
85
- return normalizeWorkspaceRecord(row);
87
+ return queryFirst({ slug: normalizedSlug }, options);
86
88
  }
87
89
 
88
90
  async function findPersonalByOwnerUserId(userId, options = {}) {
@@ -91,41 +93,78 @@ function createRepository(knex) {
91
93
  return null;
92
94
  }
93
95
 
94
- const client = options?.trx || knex;
95
- const row = await client("workspaces as w")
96
- .where({ "w.owner_user_id": normalizedUserId, "w.is_personal": 1 })
97
- .orderBy("w.id", "asc")
98
- .select(workspaceSelectColumns())
99
- .first();
100
- return normalizeWorkspaceRecord(row);
96
+ const result = await api.resources.workspaces.query(
97
+ {
98
+ queryParams: {
99
+ filters: {
100
+ owner: normalizedUserId,
101
+ isPersonal: true
102
+ }
103
+ },
104
+ transaction: options?.trx || null,
105
+ simplified: false
106
+ },
107
+ createJsonRestContext(options?.context || null)
108
+ );
109
+
110
+ const rows = Array.isArray(simplifyJsonApiDocument(result))
111
+ ? simplifyJsonApiDocument(result).map((row) => normalizeWorkspaceRecord(row)).filter(Boolean)
112
+ : [];
113
+ rows.sort((left, right) => {
114
+ const leftId = Number(left?.id);
115
+ const rightId = Number(right?.id);
116
+ if (Number.isFinite(leftId) && Number.isFinite(rightId)) {
117
+ return leftId - rightId;
118
+ }
119
+ return String(left?.id || "").localeCompare(String(right?.id || ""));
120
+ });
121
+ return rows[0] || null;
101
122
  }
102
123
 
103
124
  async function insert(payload = {}, options = {}) {
104
- const client = options?.trx || knex;
105
- const normalizedPayload = normalizeCreatePayload(payload);
106
- const ownerUserId = normalizeRecordId(normalizedPayload.ownerUserId, { fallback: null });
125
+ const source = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
126
+ const ownerUserId = normalizeRecordId(source.ownerUserId, { fallback: null });
107
127
  if (!ownerUserId) {
108
128
  throw new TypeError("workspacesRepository.insert requires ownerUserId.");
109
129
  }
110
130
 
111
131
  const createPayload = {
112
- ...normalizedPayload,
113
- ownerUserId,
114
- isPersonal: normalizedPayload.isPersonal === true,
115
- avatarUrl: normalizeText(normalizedPayload.avatarUrl)
132
+ ...(Object.hasOwn(source, "slug") ? { slug: source.slug } : {}),
133
+ ...(Object.hasOwn(source, "name") ? { name: source.name } : {}),
134
+ ...(Object.hasOwn(source, "isPersonal") ? { isPersonal: source.isPersonal } : {}),
135
+ ...(Object.hasOwn(source, "avatarUrl") ? { avatarUrl: source.avatarUrl } : {})
116
136
  };
117
137
 
118
138
  try {
119
- return await resourceRuntime.create(createPayload, {
120
- ...options,
121
- trx: client,
122
- include: "none"
123
- });
139
+ const created = await api.resources.workspaces.post(
140
+ {
141
+ inputRecord: createJsonApiInputRecord(
142
+ RESOURCE_TYPE,
143
+ {
144
+ ...createPayload,
145
+ createdAt: new Date(),
146
+ updatedAt: new Date()
147
+ },
148
+ {
149
+ relationships: createWorkspaceRelationships({ ownerUserId })
150
+ }
151
+ ),
152
+ transaction: options?.trx || null,
153
+ simplified: false
154
+ },
155
+ createJsonRestContext(options?.context || null)
156
+ );
157
+
158
+ return normalizeWorkspaceRecord(simplifyJsonApiDocument(created));
124
159
  } catch (error) {
125
160
  if (!isDuplicateEntryError(error)) {
126
161
  throw error;
127
162
  }
128
- const bySlug = await findBySlug(createPayload.slug, { trx: client });
163
+ if (!Object.hasOwn(createPayload, "slug")) {
164
+ throw error;
165
+ }
166
+
167
+ const bySlug = await findBySlug(createPayload.slug, options);
129
168
  if (bySlug) {
130
169
  return bySlug;
131
170
  }
@@ -139,10 +178,30 @@ function createRepository(knex) {
139
178
  return null;
140
179
  }
141
180
 
142
- return resourceRuntime.updateById(normalizedWorkspaceId, patch, {
143
- ...options,
144
- include: "none"
145
- });
181
+ const sourcePatch = patch && typeof patch === "object" && !Array.isArray(patch) ? patch : {};
182
+ const workspacePatch = {
183
+ ...sourcePatch,
184
+ updatedAt: new Date()
185
+ };
186
+ const relationships = createWorkspaceRelationships(sourcePatch);
187
+
188
+ const updated = await api.resources.workspaces.patch(
189
+ {
190
+ inputRecord: createJsonApiInputRecord(
191
+ RESOURCE_TYPE,
192
+ workspacePatch,
193
+ {
194
+ id: normalizedWorkspaceId,
195
+ relationships
196
+ }
197
+ ),
198
+ transaction: options?.trx || null,
199
+ simplified: false
200
+ },
201
+ createJsonRestContext(options?.context || null)
202
+ );
203
+
204
+ return normalizeWorkspaceRecord(simplifyJsonApiDocument(updated));
146
205
  }
147
206
 
148
207
  async function listForUserId(userId, options = {}) {
@@ -151,16 +210,50 @@ function createRepository(knex) {
151
210
  return [];
152
211
  }
153
212
 
154
- const client = options?.trx || knex;
155
- const rows = await client("workspace_memberships as wm")
156
- .join("workspaces as w", "w.id", "wm.workspace_id")
157
- .where({ "wm.user_id": normalizedUserId })
158
- .whereNull("w.deleted_at")
159
- .orderBy("w.is_personal", "desc")
160
- .orderBy("w.id", "asc")
161
- .select(workspaceSelectColumns({ includeMembership: true }));
213
+ const result = await api.resources.workspaceMemberships.query(
214
+ {
215
+ queryParams: {
216
+ filters: {
217
+ user: normalizedUserId,
218
+ status: "active"
219
+ },
220
+ include: ["workspace"]
221
+ },
222
+ transaction: options?.trx || null,
223
+ simplified: false
224
+ },
225
+ createJsonRestContext(options?.context || null)
226
+ );
227
+
228
+ const rows = Array.isArray(simplifyJsonApiDocument(result)) ? simplifyJsonApiDocument(result) : [];
229
+ const workspaces = rows
230
+ .map((row) => {
231
+ const workspace = normalizeWorkspaceRecord(row?.workspace);
232
+ if (!workspace || workspace.deletedAt != null) {
233
+ return null;
234
+ }
235
+
236
+ return {
237
+ ...workspace,
238
+ roleSid: row?.roleSid,
239
+ membershipStatus: row?.status
240
+ };
241
+ })
242
+ .filter(Boolean);
243
+
244
+ workspaces.sort((left, right) => {
245
+ if (left.isPersonal !== right.isPersonal) {
246
+ return left.isPersonal ? -1 : 1;
247
+ }
248
+ const leftId = Number(left?.id);
249
+ const rightId = Number(right?.id);
250
+ if (Number.isFinite(leftId) && Number.isFinite(rightId)) {
251
+ return leftId - rightId;
252
+ }
253
+ return String(left?.id || "").localeCompare(String(right?.id || ""));
254
+ });
162
255
 
163
- return rows.map(normalizeMembershipWorkspaceRow).filter(Boolean);
256
+ return workspaces;
164
257
  }
165
258
 
166
259
  return Object.freeze({
@@ -174,4 +267,4 @@ function createRepository(knex) {
174
267
  });
175
268
  }
176
269
 
177
- export { createRepository, normalizeWorkspaceRecord, normalizeMembershipWorkspaceRow };
270
+ export { createRepository };
@@ -12,7 +12,6 @@ import {
12
12
  } from "../formatters/workspaceFormatter.js";
13
13
  import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/actions/textNormalization";
14
14
  import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
15
- import { authenticatedUserValidator } from "../validators/authenticatedUserValidator.js";
16
15
 
17
16
  function toSlugPart(value) {
18
17
  const normalized = normalizeLowerText(value)
@@ -105,38 +104,38 @@ function createService({
105
104
  }
106
105
 
107
106
  async function ensurePersonalWorkspaceForUser(user, options = {}) {
108
- const normalizedUser = authenticatedUserValidator.normalize(user);
109
- if (!normalizedUser) {
107
+ const normalizedUserId = normalizeRecordId(user?.id, { fallback: null });
108
+ if (!normalizedUserId) {
110
109
  throw new AppError(400, "Invalid authenticated user payload.");
111
110
  }
112
111
 
113
- const existing = await workspacesRepository.findPersonalByOwnerUserId(normalizedUser.id, options);
112
+ const existing = await workspacesRepository.findPersonalByOwnerUserId(normalizedUserId, options);
114
113
  if (existing) {
115
- await workspaceMembershipsRepository.ensureOwnerMembership(existing.id, normalizedUser.id, options);
114
+ await workspaceMembershipsRepository.ensureOwnerMembership(existing.id, normalizedUserId, options);
116
115
  await ensureWorkspaceSettingsForWorkspace(existing, options);
117
116
  return existing;
118
117
  }
119
118
 
120
- const slug = await ensureUniqueWorkspaceSlug(buildWorkspaceBaseSlug(normalizedUser), options);
119
+ const slug = await ensureUniqueWorkspaceSlug(buildWorkspaceBaseSlug(user), options);
121
120
  const inserted = await workspacesRepository.insert(
122
121
  {
123
122
  slug,
124
- name: buildWorkspaceName(normalizedUser),
125
- ownerUserId: normalizedUser.id,
123
+ name: buildWorkspaceName(user),
124
+ ownerUserId: normalizedUserId,
126
125
  isPersonal: true,
127
126
  avatarUrl: ""
128
127
  },
129
128
  options
130
129
  );
131
130
 
132
- await workspaceMembershipsRepository.ensureOwnerMembership(inserted.id, normalizedUser.id, options);
131
+ await workspaceMembershipsRepository.ensureOwnerMembership(inserted.id, normalizedUserId, options);
133
132
  await ensureWorkspaceSettingsForWorkspace(inserted, options);
134
133
  return inserted;
135
134
  }
136
135
 
137
136
  async function listWorkspacesForUser(user, options = {}) {
138
- const normalizedUser = authenticatedUserValidator.normalize(user);
139
- if (!normalizedUser) {
137
+ const normalizedUserId = normalizeRecordId(user?.id, { fallback: null });
138
+ if (!normalizedUserId) {
140
139
  return [];
141
140
  }
142
141
 
@@ -144,7 +143,7 @@ function createService({
144
143
  return [];
145
144
  }
146
145
 
147
- const list = await workspacesRepository.listForUserId(normalizedUser.id, options);
146
+ const list = await workspacesRepository.listForUserId(normalizedUserId, options);
148
147
  const accessible = list
149
148
  .map((entry) => mapWorkspaceSummary(entry, { roleSid: entry.roleSid, status: entry.membershipStatus }))
150
149
  .filter((entry) => entry.isAccessible);
@@ -156,9 +155,9 @@ function createService({
156
155
  return listWorkspacesForUser(user, options);
157
156
  }
158
157
 
159
- async function provisionWorkspaceForNewUser(user, options = {}) {
160
- const normalizedUser = authenticatedUserValidator.normalize(user);
161
- if (!normalizedUser) {
158
+ async function ensureProvisionedWorkspaceForAuthenticatedUser(user, options = {}) {
159
+ const normalizedUserId = normalizeRecordId(user?.id, { fallback: null });
160
+ if (!normalizedUserId) {
162
161
  throw new AppError(400, "Invalid authenticated user payload.");
163
162
  }
164
163
 
@@ -166,12 +165,15 @@ function createService({
166
165
  return null;
167
166
  }
168
167
 
169
- return ensurePersonalWorkspaceForUser(normalizedUser, options);
168
+ return ensurePersonalWorkspaceForUser({
169
+ ...user,
170
+ id: normalizedUserId
171
+ }, options);
170
172
  }
171
173
 
172
174
  async function createWorkspaceForAuthenticatedUser(user, payload = {}, options = {}) {
173
- const normalizedUser = authenticatedUserValidator.normalize(user);
174
- if (!normalizedUser) {
175
+ const normalizedUserId = normalizeRecordId(user?.id, { fallback: null });
176
+ if (!normalizedUserId) {
175
177
  throw new AppError(401, "Authentication required.");
176
178
  }
177
179
 
@@ -190,14 +192,14 @@ function createService({
190
192
  {
191
193
  slug,
192
194
  name: createInput.name,
193
- ownerUserId: normalizedUser.id,
195
+ ownerUserId: normalizedUserId,
194
196
  isPersonal: false,
195
197
  avatarUrl: ""
196
198
  },
197
199
  options
198
200
  );
199
201
 
200
- await workspaceMembershipsRepository.ensureOwnerMembership(inserted.id, normalizedUser.id, options);
202
+ await workspaceMembershipsRepository.ensureOwnerMembership(inserted.id, normalizedUserId, options);
201
203
  await ensureWorkspaceSettingsForWorkspace(inserted, options);
202
204
  return inserted;
203
205
  }
@@ -213,8 +215,8 @@ function createService({
213
215
  }
214
216
 
215
217
  async function resolveWorkspaceContextForUserBySlug(user, workspaceSlug, options = {}) {
216
- const normalizedUser = authenticatedUserValidator.normalize(user);
217
- if (!normalizedUser) {
218
+ const normalizedUserId = normalizeRecordId(user?.id, { fallback: null });
219
+ if (!normalizedUserId) {
218
220
  throw new AppError(401, "Authentication required.");
219
221
  }
220
222
 
@@ -235,16 +237,16 @@ function createService({
235
237
 
236
238
  let membership = await workspaceMembershipsRepository.findByWorkspaceIdAndUserId(
237
239
  workspace.id,
238
- normalizedUser.id,
240
+ normalizedUserId,
239
241
  options
240
242
  );
241
243
  const actorOwnsWorkspace =
242
244
  normalizeRecordId(workspace.ownerUserId, { fallback: null }) ===
243
- normalizeRecordId(normalizedUser.id, { fallback: null });
245
+ normalizedUserId;
244
246
  const membershipIsActive = normalizeLowerText(membership?.status) === "active";
245
247
 
246
248
  if (!membershipIsActive && actorOwnsWorkspace) {
247
- membership = await workspaceMembershipsRepository.ensureOwnerMembership(workspace.id, normalizedUser.id, options);
249
+ membership = await workspaceMembershipsRepository.ensureOwnerMembership(workspace.id, normalizedUserId, options);
248
250
  }
249
251
 
250
252
  if (!membership || normalizeLowerText(membership.status) !== "active") {
@@ -268,7 +270,7 @@ function createService({
268
270
  buildWorkspaceBaseSlug,
269
271
  hashInviteToken,
270
272
  ensurePersonalWorkspaceForUser,
271
- provisionWorkspaceForNewUser,
273
+ ensureProvisionedWorkspaceForAuthenticatedUser,
272
274
  createWorkspaceForAuthenticatedUser,
273
275
  getWorkspaceForAuthenticatedUser,
274
276
  updateWorkspaceForAuthenticatedUser,
@@ -1,62 +1,45 @@
1
- import { Type } from "@fastify/type-provider-typebox";
2
- import { normalizeObjectInput } from "@jskit-ai/kernel/shared/validators/inputNormalization";
3
- import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
4
-
5
- function normalizeRouteParams(input = {}) {
6
- const source = normalizeObjectInput(input);
7
- const normalized = {};
8
-
9
- if (Object.hasOwn(source, "workspaceSlug")) {
10
- normalized.workspaceSlug = normalizeText(source.workspaceSlug).toLowerCase();
11
- }
12
-
13
- if (Object.hasOwn(source, "memberUserId")) {
14
- normalized.memberUserId = normalizeText(source.memberUserId);
1
+ import { createSchema } from "json-rest-schema";
2
+ import { deepFreeze } from "@jskit-ai/kernel/shared/support/deepFreeze";
3
+
4
+ const routeParamsSchema = createSchema({
5
+ workspaceSlug: {
6
+ type: "string",
7
+ required: false,
8
+ lowercase: true,
9
+ minLength: 1
10
+ },
11
+ memberUserId: {
12
+ type: "id",
13
+ required: false
14
+ },
15
+ inviteId: {
16
+ type: "id",
17
+ required: false
18
+ },
19
+ provider: {
20
+ type: "string",
21
+ required: false,
22
+ minLength: 1
15
23
  }
24
+ });
16
25
 
17
- if (Object.hasOwn(source, "inviteId")) {
18
- normalized.inviteId = normalizeText(source.inviteId);
19
- }
20
-
21
- if (Object.hasOwn(source, "provider")) {
22
- normalized.provider = normalizeText(source.provider);
23
- }
24
-
25
- return normalized;
26
- }
27
-
28
- function normalizeWorkspaceSlugParams(input = {}) {
29
- const source = normalizeObjectInput(input);
30
- const normalized = {};
31
-
32
- if (Object.hasOwn(source, "workspaceSlug")) {
33
- normalized.workspaceSlug = normalizeText(source.workspaceSlug).toLowerCase();
26
+ const workspaceSlugParamsSchema = createSchema({
27
+ workspaceSlug: {
28
+ type: "string",
29
+ required: false,
30
+ lowercase: true,
31
+ minLength: 1
34
32
  }
33
+ });
35
34
 
36
- return normalized;
37
- }
38
-
39
- const routeParamsValidator = Object.freeze({
40
- schema: Type.Object(
41
- {
42
- workspaceSlug: Type.Optional(Type.String({ minLength: 1 })),
43
- memberUserId: Type.Optional(Type.String({ minLength: 1 })),
44
- inviteId: Type.Optional(Type.String({ minLength: 1 })),
45
- provider: Type.Optional(Type.String({ minLength: 1 }))
46
- },
47
- { additionalProperties: false }
48
- ),
49
- normalize: normalizeRouteParams
35
+ const routeParamsValidator = deepFreeze({
36
+ schema: routeParamsSchema,
37
+ mode: "patch"
50
38
  });
51
39
 
52
- const workspaceSlugParamsValidator = Object.freeze({
53
- schema: Type.Object(
54
- {
55
- workspaceSlug: Type.Optional(Type.String({ minLength: 1 }))
56
- },
57
- { additionalProperties: false }
58
- ),
59
- normalize: normalizeWorkspaceSlugParams
40
+ const workspaceSlugParamsValidator = deepFreeze({
41
+ schema: workspaceSlugParamsSchema,
42
+ mode: "patch"
60
43
  });
61
44
 
62
45
  export { routeParamsValidator, workspaceSlugParamsValidator };
@@ -2,6 +2,7 @@ import {
2
2
  registerActionContextContributor
3
3
  } from "@jskit-ai/kernel/server/actions";
4
4
  import { registerRouteVisibilityResolver } from "@jskit-ai/kernel/server/http";
5
+ import { registerAuthPolicyContextResolver } from "@jskit-ai/auth-core/server/authPolicyContextResolverRegistry";
5
6
  import { resolveAppConfig } from "@jskit-ai/kernel/server/support";
6
7
  import { registerProfileSyncLifecycleContributor } from "@jskit-ai/users-core/server/profileSyncLifecycleContributorRegistry";
7
8
  import { createService as createWorkspaceService } from "./common/services/workspaceContextService.js";
@@ -66,12 +67,12 @@ function registerWorkspaceCore(app) {
66
67
  return Object.freeze({
67
68
  contributorId: "workspaces.core.profileSync",
68
69
  order: 100,
69
- async afterIdentityProfileSynced({ profile, created, options } = {}) {
70
- if (!created || !profile || typeof workspaceService?.provisionWorkspaceForNewUser !== "function") {
70
+ async afterIdentityProfileSynced({ profile, options } = {}) {
71
+ if (!profile || typeof workspaceService?.ensureProvisionedWorkspaceForAuthenticatedUser !== "function") {
71
72
  return;
72
73
  }
73
74
 
74
- await workspaceService.provisionWorkspaceForNewUser(profile, options);
75
+ await workspaceService.ensureProvisionedWorkspaceForAuthenticatedUser(profile, options);
75
76
  }
76
77
  });
77
78
  });
@@ -84,13 +85,11 @@ function registerWorkspaceCore(app) {
84
85
  });
85
86
  });
86
87
 
87
- if (typeof app.has !== "function" || !app.has("auth.policy.contextResolver")) {
88
- app.singleton("auth.policy.contextResolver", (scope) =>
89
- createWorkspaceAuthPolicyContextResolver({
90
- workspaceService: scope.make("workspaces.service")
91
- })
92
- );
93
- }
88
+ registerAuthPolicyContextResolver(app, "workspaces.core.authPolicyContextResolver", (scope) =>
89
+ createWorkspaceAuthPolicyContextResolver({
90
+ workspaceService: scope.make("workspaces.service")
91
+ })
92
+ );
94
93
 
95
94
  registerRouteVisibilityResolver(app, "users.core.workspace.routeVisibilityResolver", (scope) =>
96
95
  createWorkspaceRouteVisibilityResolver({
@@ -1,3 +1,4 @@
1
+ import { INTERNAL_JSON_REST_API } from "@jskit-ai/json-rest-api-core/server/jsonRestApiHost";
1
2
  import { createRepository as createWorkspacesRepository } from "./common/repositories/workspacesRepository.js";
2
3
  import { createRepository as createWorkspaceMembershipsRepository } from "./common/repositories/workspaceMembershipsRepository.js";
3
4
  import { createRepository as createWorkspaceInvitesRepository } from "./common/repositories/workspaceInvitesRepository.js";
@@ -8,18 +9,21 @@ function registerWorkspaceRepositories(app) {
8
9
  }
9
10
 
10
11
  app.singleton("internal.repository.workspaces", (scope) => {
12
+ const api = scope.make(INTERNAL_JSON_REST_API);
11
13
  const knex = scope.make("jskit.database.knex");
12
- return createWorkspacesRepository(knex);
14
+ return createWorkspacesRepository({ api, knex });
13
15
  });
14
16
 
15
17
  app.singleton("internal.repository.workspace-memberships", (scope) => {
18
+ const api = scope.make(INTERNAL_JSON_REST_API);
16
19
  const knex = scope.make("jskit.database.knex");
17
- return createWorkspaceMembershipsRepository(knex);
20
+ return createWorkspaceMembershipsRepository({ api, knex });
18
21
  });
19
22
 
20
23
  app.singleton("internal.repository.workspace-invites", (scope) => {
24
+ const api = scope.make(INTERNAL_JSON_REST_API);
21
25
  const knex = scope.make("jskit.database.knex");
22
- return createWorkspaceInvitesRepository(knex);
26
+ return createWorkspaceInvitesRepository({ api, knex });
23
27
  });
24
28
  }
25
29
 
@@ -5,7 +5,7 @@ import { resolveWorkspace } from "./resolveWorkspace.js";
5
5
  function createWorkspaceServerScopeSupport() {
6
6
  return Object.freeze({
7
7
  available: true,
8
- paramsValidator: workspaceSlugParamsValidator,
8
+ params: workspaceSlugParamsValidator,
9
9
  buildInputFromRouteParams(params = {}) {
10
10
  return buildWorkspaceInputFromRouteParams(params);
11
11
  },