@jskit-ai/users-core 0.1.42 → 0.1.44

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 (55) hide show
  1. package/package.descriptor.mjs +6 -6
  2. package/package.json +6 -6
  3. package/src/server/accountProfile/avatarStorageService.js +3 -3
  4. package/src/server/common/contributors/workspaceRouteVisibilityResolver.js +12 -13
  5. package/src/server/common/formatters/workspaceFormatter.js +3 -2
  6. package/src/server/common/repositories/repositoryUtils.js +12 -3
  7. package/src/server/common/repositories/userSettingsRepository.js +35 -11
  8. package/src/server/common/repositories/usersRepository.js +44 -27
  9. package/src/server/common/repositories/workspaceInvitesRepository.js +49 -13
  10. package/src/server/common/repositories/workspaceMembershipsRepository.js +55 -22
  11. package/src/server/common/repositories/workspacesRepository.js +41 -11
  12. package/src/server/common/services/accountContextService.js +3 -2
  13. package/src/server/common/services/authProfileSyncService.js +7 -5
  14. package/src/server/common/services/workspaceContextService.js +4 -1
  15. package/src/server/common/support/realtimeServiceEvents.js +4 -3
  16. package/src/server/common/validators/authenticatedUserValidator.js +5 -4
  17. package/src/server/consoleSettings/consoleService.js +3 -3
  18. package/src/server/consoleSettings/consoleSettingsRepository.js +10 -6
  19. package/src/server/usersBootstrapContributor.js +7 -3
  20. package/src/server/workspaceBootstrapContributor.js +5 -1
  21. package/src/server/workspaceMembers/registerWorkspaceMembers.js +6 -4
  22. package/src/server/workspaceMembers/workspaceMembersService.js +23 -11
  23. package/src/server/workspacePendingInvitations/registerWorkspacePendingInvitations.js +5 -4
  24. package/src/server/workspacePendingInvitations/workspacePendingInvitationsService.js +3 -2
  25. package/src/server/workspaceSettings/workspaceSettingsRepository.js +29 -10
  26. package/src/server/workspaceSettings/workspaceSettingsService.js +3 -2
  27. package/src/shared/resources/workspaceMembersResource.js +25 -21
  28. package/src/shared/resources/workspacePendingInvitationsResource.js +7 -12
  29. package/src/shared/resources/workspaceResource.js +13 -9
  30. package/src/shared/resources/workspaceSettingsResource.js +7 -5
  31. package/templates/migrations/users_core_console_owner.cjs +1 -1
  32. package/templates/migrations/users_core_generic_initial.cjs +4 -4
  33. package/templates/migrations/users_core_profile_username.cjs +1 -1
  34. package/test/authProfileSyncService.test.js +7 -4
  35. package/test/avatarStorageService.test.js +3 -3
  36. package/test/consoleService.test.js +9 -9
  37. package/test/registerServiceRealtimeEvents.test.js +9 -9
  38. package/test/repositoryContracts.test.js +40 -0
  39. package/test/usersBootstrapContributor.test.js +4 -4
  40. package/test/workspaceBootstrapContributor.test.js +1 -1
  41. package/test/workspaceInvitesRepository.test.js +3 -3
  42. package/test/workspaceMembersService.test.js +34 -34
  43. package/test/workspacePendingInvitationsResource.test.js +4 -4
  44. package/test/workspacePendingInvitationsService.test.js +11 -11
  45. package/test/workspaceRouteVisibilityResolver.test.js +6 -6
  46. package/test/workspaceService.test.js +33 -33
  47. package/test/workspaceSettingsRepository.test.js +7 -6
  48. package/test/workspaceSettingsResource.test.js +2 -2
  49. package/src/server/common/README.md +0 -20
  50. package/src/server/common/contributors/README.md +0 -11
  51. package/src/server/common/formatters/README.md +0 -11
  52. package/src/server/common/repositories/README.md +0 -24
  53. package/src/server/common/routes/README.md +0 -11
  54. package/src/server/common/services/README.md +0 -12
  55. package/src/server/common/validators/README.md +0 -11
@@ -1,3 +1,4 @@
1
+ import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
1
2
  import { buildInviteToken, hashInviteToken } from "@jskit-ai/auth-core/server/inviteTokens";
2
3
  import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
3
4
  import { OWNER_ROLE_ID, createWorkspaceRoleCatalog, cloneWorkspaceRoleCatalog } from "../../shared/roles.js";
@@ -61,8 +62,11 @@ function createService({
61
62
  }
62
63
 
63
64
  async function updateMemberRole(workspace, payload = {}, options = {}) {
64
- const memberUserId = payload.memberUserId;
65
+ const memberUserId = normalizeRecordId(payload.memberUserId, { fallback: null });
65
66
  const roleSid = payload.roleSid;
67
+ if (!memberUserId) {
68
+ throw new AppError(400, "Validation failed.");
69
+ }
66
70
  if (!assignableRoleIds.includes(roleSid)) {
67
71
  throw new AppError(400, "Validation failed.", {
68
72
  details: {
@@ -77,7 +81,7 @@ function createService({
77
81
  if (!existingMembership || existingMembership.status !== "active") {
78
82
  throw new AppError(404, "Member not found.");
79
83
  }
80
- if (Number(memberUserId) === Number(workspace.ownerUserId) || existingMembership.roleSid === OWNER_ROLE_ID) {
84
+ if (memberUserId === normalizeRecordId(workspace.ownerUserId, { fallback: null }) || existingMembership.roleSid === OWNER_ROLE_ID) {
81
85
  throw new AppError(409, "Cannot change workspace owner role.");
82
86
  }
83
87
 
@@ -95,13 +99,16 @@ function createService({
95
99
  }
96
100
 
97
101
  async function removeMember(workspace, payload = {}, options = {}) {
98
- const memberUserId = payload.memberUserId;
102
+ const memberUserId = normalizeRecordId(payload.memberUserId, { fallback: null });
103
+ if (!memberUserId) {
104
+ throw new AppError(400, "Validation failed.");
105
+ }
99
106
 
100
107
  const existingMembership = await workspaceMembershipsRepository.findByWorkspaceIdAndUserId(workspace.id, memberUserId, options);
101
108
  if (!existingMembership || existingMembership.status !== "active") {
102
109
  throw new AppError(404, "Member not found.");
103
110
  }
104
- if (Number(memberUserId) === Number(workspace.ownerUserId) || existingMembership.roleSid === OWNER_ROLE_ID) {
111
+ if (memberUserId === normalizeRecordId(workspace.ownerUserId, { fallback: null }) || existingMembership.roleSid === OWNER_ROLE_ID) {
105
112
  throw new AppError(409, "Cannot remove workspace owner.");
106
113
  }
107
114
 
@@ -155,13 +162,13 @@ function createService({
155
162
  roleSid,
156
163
  status: "pending",
157
164
  tokenHash,
158
- invitedByUserId: Number(user?.id || 0) || null,
165
+ invitedByUserId: normalizeRecordId(user?.id, { fallback: null }),
159
166
  expiresAt: new Date(Date.now() + resolvedInviteExpiresInMs).toISOString()
160
167
  },
161
168
  options
162
169
  );
163
- const createdInviteId = Number(createdInvite?.id);
164
- if (!Number.isInteger(createdInviteId) || createdInviteId < 1) {
170
+ const createdInviteId = normalizeRecordId(createdInvite?.id, { fallback: null });
171
+ if (!createdInviteId) {
165
172
  throw new Error("workspaceMembersService.createInvite expected repository to return created invite id.");
166
173
  }
167
174
 
@@ -174,8 +181,13 @@ function createService({
174
181
  }
175
182
 
176
183
  async function revokeInvite(workspace, inviteId, options = {}) {
184
+ const normalizedInviteId = normalizeRecordId(inviteId, { fallback: null });
185
+ if (!normalizedInviteId) {
186
+ throw new AppError(400, "Validation failed.");
187
+ }
188
+
177
189
  const invite = await workspaceInvitesRepository.findPendingByIdForWorkspace(
178
- inviteId,
190
+ normalizedInviteId,
179
191
  workspace.id,
180
192
  options
181
193
  );
@@ -183,9 +195,9 @@ function createService({
183
195
  throw new AppError(404, "Invite not found.");
184
196
  }
185
197
 
186
- await workspaceInvitesRepository.revokeById(inviteId, options);
187
- const revokedInviteId = Number(invite?.id);
188
- if (!Number.isInteger(revokedInviteId) || revokedInviteId < 1) {
198
+ await workspaceInvitesRepository.revokeById(normalizedInviteId, options);
199
+ const revokedInviteId = normalizeRecordId(invite?.id, { fallback: null });
200
+ if (!revokedInviteId) {
189
201
  throw new Error("workspaceMembersService.revokeInvite expected repository to return pending invite id.");
190
202
  }
191
203
 
@@ -1,11 +1,12 @@
1
1
  import { withActionDefaults } from "@jskit-ai/kernel/shared/actions";
2
+ import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
2
3
  import { createService } from "./workspacePendingInvitationsService.js";
3
4
  import { workspacePendingInvitationsActions } from "./workspacePendingInvitationsActions.js";
4
5
  import { deepFreeze } from "../common/support/deepFreeze.js";
5
6
 
6
7
  function workspaceAudienceFromEntityId({ event } = {}) {
7
- const workspaceId = Number(event?.entityId);
8
- if (!Number.isInteger(workspaceId) || workspaceId < 1) {
8
+ const workspaceId = normalizeRecordId(event?.entityId, { fallback: null });
9
+ if (!workspaceId) {
9
10
  return "none";
10
11
  }
11
12
  return {
@@ -14,7 +15,7 @@ function workspaceAudienceFromEntityId({ event } = {}) {
14
15
  }
15
16
 
16
17
  function actorUserEntityId({ options } = {}) {
17
- return Number(options?.context?.actor?.id || 0);
18
+ return normalizeRecordId(options?.context?.actor?.id, { fallback: "" });
18
19
  }
19
20
 
20
21
  function createActorUserEvent({ source, entity, realtimeEvent }) {
@@ -37,7 +38,7 @@ function createWorkspaceAudienceEvent({ entity, realtimeEvent }) {
37
38
  source: "workspace",
38
39
  entity,
39
40
  operation: "updated",
40
- entityId: ({ result }) => result?.workspaceId,
41
+ entityId: ({ result }) => normalizeRecordId(result?.workspaceId, { fallback: "" }),
41
42
  realtime: {
42
43
  event: realtimeEvent,
43
44
  audience: workspaceAudienceFromEntityId
@@ -1,6 +1,7 @@
1
1
  import { resolveInviteTokenHash } from "@jskit-ai/auth-core/server/inviteTokens";
2
2
  import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
3
3
  import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/actions/textNormalization";
4
+ import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
4
5
  import { authenticatedUserValidator } from "../common/validators/authenticatedUserValidator.js";
5
6
 
6
7
  function createService({
@@ -70,8 +71,8 @@ function createService({
70
71
  }
71
72
 
72
73
  function requireWorkspaceIdFromInvite(invite, methodName = "workspacePendingInvitationsService") {
73
- const workspaceId = Number(invite?.workspaceId);
74
- if (!Number.isInteger(workspaceId) || workspaceId < 1) {
74
+ const workspaceId = normalizeRecordId(invite?.workspaceId, { fallback: null });
75
+ if (!workspaceId) {
75
76
  throw new Error(`${methodName} expected invite workspace id.`);
76
77
  }
77
78
  return workspaceId;
@@ -1,7 +1,10 @@
1
1
  import {
2
+ normalizeDbRecordId,
3
+ normalizeRecordId,
2
4
  toIsoString,
3
5
  nowDb,
4
- isDuplicateEntryError
6
+ isDuplicateEntryError,
7
+ createWithTransaction
5
8
  } from "../common/repositories/repositoryUtils.js";
6
9
  import { normalizeObjectInput } from "@jskit-ai/kernel/shared/validators/inputNormalization";
7
10
  import { pickOwnProperties } from "@jskit-ai/kernel/shared/support";
@@ -32,6 +35,7 @@ function createRepository(knex, { defaultInvitesEnabled } = {}) {
32
35
  if (typeof knex !== "function") {
33
36
  throw new TypeError("workspaceSettingsRepository requires knex.");
34
37
  }
38
+ const withTransaction = createWithTransaction(knex);
35
39
 
36
40
  function mapRow(row) {
37
41
  if (!row) {
@@ -39,7 +43,7 @@ function createRepository(knex, { defaultInvitesEnabled } = {}) {
39
43
  }
40
44
 
41
45
  const settings = {
42
- workspaceId: Number(row.workspace_id)
46
+ workspaceId: normalizeDbRecordId(row.workspace_id, { fallback: "" })
43
47
  };
44
48
  for (const field of workspaceSettingsFields) {
45
49
  const rawValue = Object.hasOwn(row, field.dbColumn)
@@ -59,24 +63,33 @@ function createRepository(knex, { defaultInvitesEnabled } = {}) {
59
63
 
60
64
  async function findByWorkspaceId(workspaceId, options = {}) {
61
65
  const client = options?.trx || knex;
62
- const row = await client("workspace_settings").where({ workspace_id: Number(workspaceId) }).first();
66
+ const normalizedWorkspaceId = normalizeRecordId(workspaceId, { fallback: null });
67
+ if (!normalizedWorkspaceId) {
68
+ return null;
69
+ }
70
+
71
+ const row = await client("workspace_settings").where({ workspace_id: normalizedWorkspaceId }).first();
63
72
  return mapRow(row);
64
73
  }
65
74
 
66
75
  async function ensureForWorkspaceId(workspaceId, options = {}) {
67
76
  const client = options?.trx || knex;
68
- const numericWorkspaceId = Number(workspaceId);
77
+ const normalizedWorkspaceId = normalizeRecordId(workspaceId, { fallback: null });
78
+ if (!normalizedWorkspaceId) {
79
+ throw new TypeError("workspaceSettingsRepository.ensureForWorkspaceId requires a valid workspace id.");
80
+ }
81
+
69
82
  const seed = resolveWorkspaceSettingsSeed(options?.workspace, {
70
83
  defaultInvitesEnabled
71
84
  });
72
- const existing = await findByWorkspaceId(numericWorkspaceId, { trx: client });
85
+ const existing = await findByWorkspaceId(normalizedWorkspaceId, { trx: client });
73
86
  if (existing) {
74
87
  return existing;
75
88
  }
76
89
 
77
90
  try {
78
91
  const insertPayload = {
79
- workspace_id: numericWorkspaceId,
92
+ workspace_id: normalizedWorkspaceId,
80
93
  created_at: nowDb(),
81
94
  updated_at: nowDb()
82
95
  };
@@ -90,12 +103,17 @@ function createRepository(knex, { defaultInvitesEnabled } = {}) {
90
103
  }
91
104
  }
92
105
 
93
- return findByWorkspaceId(numericWorkspaceId, { trx: client });
106
+ return findByWorkspaceId(normalizedWorkspaceId, { trx: client });
94
107
  }
95
108
 
96
109
  async function updateSettingsByWorkspaceId(workspaceId, patch = {}, options = {}) {
97
110
  const client = options?.trx || knex;
98
- const ensured = await ensureForWorkspaceId(workspaceId, {
111
+ const normalizedWorkspaceId = normalizeRecordId(workspaceId, { fallback: null });
112
+ if (!normalizedWorkspaceId) {
113
+ throw new TypeError("workspaceSettingsRepository.updateSettingsByWorkspaceId requires a valid workspace id.");
114
+ }
115
+
116
+ const ensured = await ensureForWorkspaceId(normalizedWorkspaceId, {
99
117
  trx: client,
100
118
  workspace: options?.workspace
101
119
  });
@@ -119,13 +137,14 @@ function createRepository(knex, { defaultInvitesEnabled } = {}) {
119
137
  });
120
138
  }
121
139
 
122
- await client("workspace_settings").where({ workspace_id: Number(workspaceId) }).update({
140
+ await client("workspace_settings").where({ workspace_id: normalizedWorkspaceId }).update({
123
141
  ...dbPatch
124
142
  });
125
- return findByWorkspaceId(workspaceId, { trx: client });
143
+ return findByWorkspaceId(normalizedWorkspaceId, { trx: client });
126
144
  }
127
145
 
128
146
  return Object.freeze({
147
+ withTransaction,
129
148
  findByWorkspaceId,
130
149
  ensureForWorkspaceId,
131
150
  updateSettingsByWorkspaceId
@@ -1,3 +1,4 @@
1
+ import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
1
2
  import { normalizeObjectInput } from "@jskit-ai/kernel/shared/validators/inputNormalization";
2
3
  import { pickOwnProperties } from "@jskit-ai/kernel/shared/support";
3
4
  import {
@@ -33,9 +34,9 @@ function createService({
33
34
 
34
35
  return {
35
36
  workspace: {
36
- id: Number(workspace.id),
37
+ id: normalizeRecordId(workspace.id, { fallback: "" }),
37
38
  slug: String(workspace.slug || ""),
38
- ownerUserId: Number(workspace.ownerUserId)
39
+ ownerUserId: normalizeRecordId(workspace.ownerUserId, { fallback: "" })
39
40
  },
40
41
  settings,
41
42
  roleCatalog: cloneWorkspaceRoleCatalog(resolvedRoleCatalog)
@@ -1,16 +1,21 @@
1
1
  import { Type } from "@fastify/type-provider-typebox";
2
2
  import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/actions/textNormalization";
3
- import { normalizeObjectInput } from "@jskit-ai/kernel/shared/validators/inputNormalization";
4
- import { normalizePositiveInteger } from "@jskit-ai/kernel/shared/support/normalize";
3
+ import {
4
+ normalizeObjectInput,
5
+ recordIdSchema,
6
+ recordIdInputSchema,
7
+ nullableRecordIdSchema
8
+ } from "@jskit-ai/kernel/shared/validators";
9
+ import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
5
10
  import { createOperationMessages } from "../operationMessages.js";
6
11
  import { createWorkspaceRoleCatalog, OWNER_ROLE_ID } from "../roles.js";
7
12
 
8
13
  const workspaceSummaryOutputSchema = Type.Object(
9
14
  {
10
- id: Type.Integer({ minimum: 1 }),
15
+ id: recordIdSchema,
11
16
  slug: Type.String({ minLength: 1 }),
12
17
  name: Type.String({ minLength: 1 }),
13
- ownerUserId: Type.Integer({ minimum: 1 }),
18
+ ownerUserId: recordIdSchema,
14
19
  avatarUrl: Type.String()
15
20
  },
16
21
  { additionalProperties: false }
@@ -18,7 +23,7 @@ const workspaceSummaryOutputSchema = Type.Object(
18
23
 
19
24
  const memberSummaryOutputSchema = Type.Object(
20
25
  {
21
- userId: Type.Integer({ minimum: 1 }),
26
+ userId: recordIdSchema,
22
27
  roleSid: Type.String({ minLength: 1 }),
23
28
  status: Type.String({ minLength: 1 }),
24
29
  displayName: Type.String(),
@@ -30,12 +35,12 @@ const memberSummaryOutputSchema = Type.Object(
30
35
 
31
36
  const inviteSummaryOutputSchema = Type.Object(
32
37
  {
33
- id: Type.Integer({ minimum: 1 }),
38
+ id: recordIdSchema,
34
39
  email: Type.String({ minLength: 3, format: "email" }),
35
40
  roleSid: Type.String({ minLength: 1 }),
36
41
  status: Type.String({ minLength: 1 }),
37
42
  expiresAt: Type.String({ minLength: 1 }),
38
- invitedByUserId: Type.Union([Type.Integer({ minimum: 1 }), Type.Null()])
43
+ invitedByUserId: nullableRecordIdSchema
39
44
  },
40
45
  { additionalProperties: false }
41
46
  );
@@ -44,26 +49,25 @@ function normalizeWorkspaceAdminSummary(workspace) {
44
49
  const source = normalizeObjectInput(workspace);
45
50
 
46
51
  return {
47
- id: Number(source.id),
52
+ id: normalizeRecordId(source.id, { fallback: "" }),
48
53
  slug: normalizeText(source.slug),
49
54
  name: normalizeText(source.name),
50
- ownerUserId: Number(source.ownerUserId),
55
+ ownerUserId: normalizeRecordId(source.ownerUserId, { fallback: "" }),
51
56
  avatarUrl: normalizeText(source.avatarUrl)
52
57
  };
53
58
  }
54
59
 
55
60
  function normalizeMemberSummary(member, workspace) {
56
61
  const source = normalizeObjectInput(member);
62
+ const userId = normalizeRecordId(source.userId, { fallback: "" });
57
63
 
58
64
  return {
59
- userId: Number(source.userId),
65
+ userId,
60
66
  roleSid: normalizeLowerText(source.roleSid || "member") || "member",
61
67
  status: normalizeLowerText(source.status || "active") || "active",
62
68
  displayName: normalizeText(source.displayName),
63
69
  email: normalizeLowerText(source.email),
64
- isOwner:
65
- Number(source.userId) === Number(workspace.ownerUserId) ||
66
- normalizeLowerText(source.roleSid) === OWNER_ROLE_ID
70
+ isOwner: userId === workspace.ownerUserId || normalizeLowerText(source.roleSid) === OWNER_ROLE_ID
67
71
  };
68
72
  }
69
73
 
@@ -71,12 +75,12 @@ function normalizeInviteSummary(invite) {
71
75
  const source = normalizeObjectInput(invite);
72
76
 
73
77
  return {
74
- id: Number(source.id),
78
+ id: normalizeRecordId(source.id, { fallback: "" }),
75
79
  email: normalizeLowerText(source.email),
76
80
  roleSid: normalizeLowerText(source.roleSid || "member") || "member",
77
81
  status: normalizeLowerText(source.status || "pending") || "pending",
78
82
  expiresAt: source.expiresAt,
79
- invitedByUserId: source.invitedByUserId == null ? null : Number(source.invitedByUserId)
83
+ invitedByUserId: source.invitedByUserId == null ? null : normalizeRecordId(source.invitedByUserId, { fallback: null })
80
84
  };
81
85
  }
82
86
 
@@ -176,7 +180,7 @@ const updateMemberRoleBodyValidator = Object.freeze({
176
180
  const updateMemberRoleInputValidator = Object.freeze({
177
181
  schema: Type.Object(
178
182
  {
179
- memberUserId: Type.Integer({ minimum: 1 }),
183
+ memberUserId: recordIdInputSchema,
180
184
  roleSid: Type.String({ minLength: 1 })
181
185
  },
182
186
  { additionalProperties: false }
@@ -185,7 +189,7 @@ const updateMemberRoleInputValidator = Object.freeze({
185
189
  const source = normalizeObjectInput(payload);
186
190
 
187
191
  return {
188
- memberUserId: normalizePositiveInteger(source.memberUserId),
192
+ memberUserId: normalizeRecordId(source.memberUserId, { fallback: "" }),
189
193
  roleSid: normalizeLowerText(source.roleSid)
190
194
  };
191
195
  }
@@ -194,7 +198,7 @@ const updateMemberRoleInputValidator = Object.freeze({
194
198
  const removeMemberInputValidator = Object.freeze({
195
199
  schema: Type.Object(
196
200
  {
197
- memberUserId: Type.Integer({ minimum: 1 })
201
+ memberUserId: recordIdInputSchema
198
202
  },
199
203
  { additionalProperties: false }
200
204
  ),
@@ -202,7 +206,7 @@ const removeMemberInputValidator = Object.freeze({
202
206
  const source = normalizeObjectInput(payload);
203
207
 
204
208
  return {
205
- memberUserId: normalizePositiveInteger(source.memberUserId)
209
+ memberUserId: normalizeRecordId(source.memberUserId, { fallback: "" })
206
210
  };
207
211
  }
208
212
  });
@@ -228,7 +232,7 @@ const createInviteBodyValidator = Object.freeze({
228
232
  const revokeInviteInputValidator = Object.freeze({
229
233
  schema: Type.Object(
230
234
  {
231
- inviteId: Type.Integer({ minimum: 1 })
235
+ inviteId: recordIdInputSchema
232
236
  },
233
237
  { additionalProperties: false }
234
238
  ),
@@ -236,7 +240,7 @@ const revokeInviteInputValidator = Object.freeze({
236
240
  const source = normalizeObjectInput(payload);
237
241
 
238
242
  return {
239
- inviteId: normalizePositiveInteger(source.inviteId)
243
+ inviteId: normalizeRecordId(source.inviteId, { fallback: "" })
240
244
  };
241
245
  }
242
246
  });
@@ -2,20 +2,15 @@ import { Type } from "@fastify/type-provider-typebox";
2
2
  import { encodeInviteTokenHash } from "@jskit-ai/auth-core/shared/inviteTokens";
3
3
  import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/actions/textNormalization";
4
4
  import { createOperationMessages } from "../operationMessages.js";
5
- import { normalizeObjectInput } from "@jskit-ai/kernel/shared/validators/inputNormalization";
5
+ import { normalizeObjectInput, recordIdSchema } from "@jskit-ai/kernel/shared/validators";
6
+ import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
6
7
 
7
8
  function normalizePendingInvite(invite) {
8
- const id = Number(invite?.id);
9
- const workspaceId = Number(invite?.workspaceId);
9
+ const id = normalizeRecordId(invite?.id, { fallback: null });
10
+ const workspaceId = normalizeRecordId(invite?.workspaceId, { fallback: null });
10
11
  const tokenHash = normalizeText(invite?.tokenHash);
11
12
 
12
- if (!Number.isInteger(id) || id < 1) {
13
- return null;
14
- }
15
- if (!Number.isInteger(workspaceId) || workspaceId < 1) {
16
- return null;
17
- }
18
- if (!tokenHash) {
13
+ if (!id || !workspaceId || !tokenHash) {
19
14
  return null;
20
15
  }
21
16
 
@@ -39,8 +34,8 @@ function normalizePendingInviteList(invites) {
39
34
  const pendingInviteRecordValidator = Object.freeze({
40
35
  schema: Type.Object(
41
36
  {
42
- id: Type.Integer({ minimum: 1 }),
43
- workspaceId: Type.Integer({ minimum: 1 }),
37
+ id: recordIdSchema,
38
+ workspaceId: recordIdSchema,
44
39
  workspaceSlug: Type.String({ minLength: 1 }),
45
40
  workspaceName: Type.String({ minLength: 1 }),
46
41
  workspaceAvatarUrl: Type.String(),
@@ -2,8 +2,11 @@ import { Type } from "typebox";
2
2
  import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/actions/textNormalization";
3
3
  import {
4
4
  normalizeObjectInput,
5
- createCursorListValidator
5
+ createCursorListValidator,
6
+ recordIdSchema,
7
+ recordIdInputSchema
6
8
  } from "@jskit-ai/kernel/shared/validators";
9
+ import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
7
10
 
8
11
  function normalizeWorkspaceAvatarUrl(value) {
9
12
  const avatarUrl = normalizeText(value);
@@ -31,7 +34,7 @@ function normalizeWorkspaceInput(payload = {}) {
31
34
  normalized.name = normalizeText(source.name);
32
35
  }
33
36
  if (Object.hasOwn(source, "ownerUserId")) {
34
- normalized.ownerUserId = Number(source.ownerUserId);
37
+ normalized.ownerUserId = normalizeRecordId(source.ownerUserId, { fallback: "" });
35
38
  }
36
39
  if (Object.hasOwn(source, "avatarUrl")) {
37
40
  normalized.avatarUrl = normalizeWorkspaceAvatarUrl(source.avatarUrl);
@@ -47,10 +50,10 @@ function normalizeWorkspaceOutput(payload = {}) {
47
50
  const source = normalizeObjectInput(payload);
48
51
 
49
52
  return {
50
- id: Number(source.id),
53
+ id: normalizeRecordId(source.id, { fallback: "" }),
51
54
  slug: normalizeLowerText(source.slug),
52
55
  name: normalizeText(source.name),
53
- ownerUserId: Number(source.ownerUserId),
56
+ ownerUserId: normalizeRecordId(source.ownerUserId, { fallback: "" }),
54
57
  avatarUrl: normalizeText(source.avatarUrl)
55
58
  };
56
59
  }
@@ -59,7 +62,7 @@ function normalizeWorkspaceListItemOutput(payload = {}) {
59
62
  const source = normalizeObjectInput(payload);
60
63
 
61
64
  return {
62
- id: Number(source.id),
65
+ id: normalizeRecordId(source.id, { fallback: "" }),
63
66
  slug: normalizeLowerText(source.slug),
64
67
  name: normalizeText(source.name),
65
68
  avatarUrl: normalizeText(source.avatarUrl),
@@ -70,10 +73,10 @@ function normalizeWorkspaceListItemOutput(payload = {}) {
70
73
 
71
74
  const responseRecordSchema = Type.Object(
72
75
  {
73
- id: Type.Integer({ minimum: 1 }),
76
+ id: recordIdSchema,
74
77
  slug: Type.String({ minLength: 1 }),
75
78
  name: Type.String({ minLength: 1, maxLength: 160 }),
76
- ownerUserId: Type.Integer({ minimum: 1 }),
79
+ ownerUserId: recordIdSchema,
77
80
  avatarUrl: Type.String()
78
81
  },
79
82
  { additionalProperties: false }
@@ -81,7 +84,7 @@ const responseRecordSchema = Type.Object(
81
84
 
82
85
  const listItemSchema = Type.Object(
83
86
  {
84
- id: Type.Integer({ minimum: 1 }),
87
+ id: recordIdSchema,
85
88
  slug: Type.String({ minLength: 1 }),
86
89
  name: Type.String({ minLength: 1, maxLength: 160 }),
87
90
  avatarUrl: Type.String(),
@@ -94,7 +97,8 @@ const listItemSchema = Type.Object(
94
97
  const createRequestBodySchema = Type.Object(
95
98
  {
96
99
  name: Type.String({ minLength: 1, maxLength: 160 }),
97
- slug: Type.Optional(Type.String({ minLength: 1, maxLength: 120 }))
100
+ slug: Type.Optional(Type.String({ minLength: 1, maxLength: 120 })),
101
+ ownerUserId: Type.Optional(recordIdInputSchema)
98
102
  },
99
103
  { additionalProperties: false }
100
104
  );
@@ -3,8 +3,10 @@ import { normalizeText } from "@jskit-ai/kernel/shared/actions/textNormalization
3
3
  import {
4
4
  normalizeObjectInput,
5
5
  createCursorListValidator,
6
- normalizeSettingsFieldInput
6
+ normalizeSettingsFieldInput,
7
+ recordIdSchema
7
8
  } from "@jskit-ai/kernel/shared/validators";
9
+ import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
8
10
  import { workspaceSettingsFields } from "./workspaceSettingsFields.js";
9
11
  import { createWorkspaceRoleCatalog } from "../roles.js";
10
12
 
@@ -39,9 +41,9 @@ function buildResponseRecordSchema() {
39
41
  {
40
42
  workspace: Type.Object(
41
43
  {
42
- id: Type.Integer({ minimum: 1 }),
44
+ id: recordIdSchema,
43
45
  slug: Type.String({ minLength: 1 }),
44
- ownerUserId: Type.Integer({ minimum: 1 })
46
+ ownerUserId: recordIdSchema
45
47
  },
46
48
  { additionalProperties: false }
47
49
  ),
@@ -98,9 +100,9 @@ function normalizeOutput(payload = {}) {
98
100
 
99
101
  return {
100
102
  workspace: {
101
- id: Number(workspace.id),
103
+ id: normalizeRecordId(workspace.id, { fallback: "" }),
102
104
  slug: normalizeText(workspace.slug),
103
- ownerUserId: Number(workspace.ownerUserId)
105
+ ownerUserId: normalizeRecordId(workspace.ownerUserId, { fallback: "" })
104
106
  },
105
107
  settings: normalizedSettings,
106
108
  roleCatalog: hasRoleCatalog ? roleCatalog : createWorkspaceRoleCatalog()
@@ -13,7 +13,7 @@ exports.up = async function up(knex) {
13
13
  }
14
14
 
15
15
  await knex.schema.alterTable("console_settings", (table) => {
16
- table.integer("owner_user_id").unsigned().nullable();
16
+ table.bigInteger("owner_user_id").unsigned().nullable();
17
17
  });
18
18
  };
19
19
 
@@ -5,7 +5,7 @@ exports.up = async function up(knex) {
5
5
  const hasUsersTable = await knex.schema.hasTable("users");
6
6
  if (!hasUsersTable) {
7
7
  await knex.schema.createTable("users", (table) => {
8
- table.increments("id").primary();
8
+ table.bigIncrements("id").primary();
9
9
  table.string("auth_provider", 64).notNullable();
10
10
  table.string("auth_provider_user_sid", 191).notNullable();
11
11
  table.string("email", 255).notNullable();
@@ -24,7 +24,7 @@ exports.up = async function up(knex) {
24
24
  const hasUserSettingsTable = await knex.schema.hasTable("user_settings");
25
25
  if (!hasUserSettingsTable) {
26
26
  await knex.schema.createTable("user_settings", (table) => {
27
- table.integer("user_id").unsigned().primary().references("id").inTable("users").onDelete("CASCADE");
27
+ table.bigInteger("user_id").unsigned().primary().references("id").inTable("users").onDelete("CASCADE");
28
28
  table.string("theme", 32).notNullable().defaultTo("system");
29
29
  table.string("locale", 24).notNullable().defaultTo("en");
30
30
  table.string("time_zone", 64).notNullable().defaultTo("UTC");
@@ -45,8 +45,8 @@ exports.up = async function up(knex) {
45
45
  const hasConsoleSettingsTable = await knex.schema.hasTable("console_settings");
46
46
  if (!hasConsoleSettingsTable) {
47
47
  await knex.schema.createTable("console_settings", (table) => {
48
- table.integer("id").primary();
49
- table.integer("owner_user_id").unsigned().nullable().references("id").inTable("users").onDelete("SET NULL");
48
+ table.bigInteger("id").primary();
49
+ table.bigInteger("owner_user_id").unsigned().nullable().references("id").inTable("users").onDelete("SET NULL");
50
50
  table.timestamp("created_at", { useTz: false }).notNullable().defaultTo(knex.fn.now());
51
51
  table.timestamp("updated_at", { useTz: false }).notNullable().defaultTo(knex.fn.now());
52
52
  });
@@ -62,7 +62,7 @@ exports.up = async function up(knex) {
62
62
  for (const profile of profiles) {
63
63
  const nextUsername = resolveUniqueUsername(usernameBaseFromEmail(profile.email), usedUsernames);
64
64
  usedUsernames.add(nextUsername);
65
- await knex("users").where({ id: Number(profile.id) }).update({
65
+ await knex("users").where({ id: profile.id }).update({
66
66
  username: nextUsername
67
67
  });
68
68
  }
@@ -15,7 +15,7 @@ test("authProfileSyncService.syncIdentityProfile uses shared transaction for pro
15
15
  async upsert(payload, options = {}) {
16
16
  calls.push({ step: "upsert", trx: options.trx || null });
17
17
  return {
18
- id: 13,
18
+ id: "13",
19
19
  authProvider: payload.authProvider,
20
20
  authProviderUserSid: payload.authProviderUserSid,
21
21
  email: payload.email,
@@ -67,7 +67,7 @@ test("authProfileSyncService.syncIdentityProfile skips write path when profile i
67
67
  usersRepository: {
68
68
  async findByIdentity() {
69
69
  return {
70
- id: 7,
70
+ id: "7",
71
71
  authProvider: "supabase",
72
72
  authProviderUserSid: "abc-7",
73
73
  email: "tony@example.com",
@@ -84,7 +84,7 @@ test("authProfileSyncService.syncIdentityProfile skips write path when profile i
84
84
  },
85
85
  userSettingsRepository: {
86
86
  async ensureForUserId() {
87
- return { userId: 7 };
87
+ return { userId: "7" };
88
88
  }
89
89
  },
90
90
  workspaceProvisioningService: {
@@ -116,11 +116,14 @@ test("authProfileSyncService.findByIdentity normalizes provider identity input",
116
116
  },
117
117
  async upsert() {
118
118
  return null;
119
+ },
120
+ async withTransaction(work) {
121
+ return work({ trxId: "tx-3" });
119
122
  }
120
123
  },
121
124
  userSettingsRepository: {
122
125
  async ensureForUserId() {
123
- return { userId: 1 };
126
+ return { userId: "1" };
124
127
  }
125
128
  }
126
129
  });