@jskit-ai/users-core 0.1.41 → 0.1.43

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 (48) 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
@@ -1,7 +1,7 @@
1
1
  export default Object.freeze({
2
2
  packageVersion: 1,
3
3
  packageId: "@jskit-ai/users-core",
4
- version: "0.1.41",
4
+ version: "0.1.43",
5
5
  kind: "runtime",
6
6
  description: "Users/account runtime plus HTTP routes for account and console features.",
7
7
  dependsOn: [
@@ -138,11 +138,11 @@ export default Object.freeze({
138
138
  mutations: {
139
139
  dependencies: {
140
140
  runtime: {
141
- "@jskit-ai/auth-core": "0.1.30",
142
- "@jskit-ai/database-runtime": "0.1.31",
143
- "@jskit-ai/http-runtime": "0.1.30",
144
- "@jskit-ai/kernel": "0.1.31",
145
- "@jskit-ai/uploads-runtime": "0.1.9",
141
+ "@jskit-ai/auth-core": "0.1.32",
142
+ "@jskit-ai/database-runtime": "0.1.33",
143
+ "@jskit-ai/http-runtime": "0.1.32",
144
+ "@jskit-ai/kernel": "0.1.33",
145
+ "@jskit-ai/uploads-runtime": "0.1.11",
146
146
  "@fastify/type-provider-typebox": "^6.1.0",
147
147
  typebox: "^1.0.81"
148
148
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/users-core",
3
- "version": "0.1.41",
3
+ "version": "0.1.43",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -24,11 +24,11 @@
24
24
  "./shared/resources/consoleSettingsFields": "./src/shared/resources/consoleSettingsFields.js"
25
25
  },
26
26
  "dependencies": {
27
- "@jskit-ai/auth-core": "0.1.30",
28
- "@jskit-ai/database-runtime": "0.1.31",
29
- "@jskit-ai/http-runtime": "0.1.30",
30
- "@jskit-ai/kernel": "0.1.31",
31
- "@jskit-ai/uploads-runtime": "0.1.9",
27
+ "@jskit-ai/auth-core": "0.1.32",
28
+ "@jskit-ai/database-runtime": "0.1.33",
29
+ "@jskit-ai/http-runtime": "0.1.32",
30
+ "@jskit-ai/kernel": "0.1.33",
31
+ "@jskit-ai/uploads-runtime": "0.1.11",
32
32
  "@fastify/type-provider-typebox": "^6.1.0",
33
33
  "typebox": "^1.0.81"
34
34
  }
@@ -1,4 +1,4 @@
1
- import { parsePositiveInteger } from "@jskit-ai/kernel/server/runtime";
1
+ import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
2
2
  import {
3
3
  createUploadStorageService,
4
4
  detectCommonMimeTypeFromBuffer
@@ -7,9 +7,9 @@ import {
7
7
  const AVATAR_STORAGE_PREFIX = "users/avatars";
8
8
 
9
9
  function buildAvatarStorageKey(userId) {
10
- const normalizedUserId = parsePositiveInteger(userId);
10
+ const normalizedUserId = normalizeRecordId(userId, { fallback: null });
11
11
  if (!normalizedUserId) {
12
- throw new TypeError("Avatar storage requires a positive integer user id.");
12
+ throw new TypeError("Avatar storage requires a valid user id.");
13
13
  }
14
14
 
15
15
  return `${AVATAR_STORAGE_PREFIX}/${normalizedUserId}/avatar`;
@@ -1,18 +1,17 @@
1
- import { normalizeOpaqueId, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
2
- import { parsePositiveInteger } from "@jskit-ai/kernel/server/runtime";
1
+ import { normalizeOpaqueId, normalizeRecordId, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
3
2
 
4
- function buildVisibilityContribution({ visibility, scopeOwnerId = 0, userOwnerId = null } = {}) {
3
+ function buildVisibilityContribution({ visibility, scopeOwnerId = null, userId = null } = {}) {
5
4
  const requiresActorScope = visibility === "workspace_user";
6
5
  const contribution = {
7
6
  scopeKind: requiresActorScope ? "workspace_user" : "workspace",
8
7
  requiresActorScope
9
8
  };
10
9
 
11
- if (scopeOwnerId > 0) {
10
+ if (scopeOwnerId) {
12
11
  contribution.scopeOwnerId = scopeOwnerId;
13
12
  }
14
- if (requiresActorScope && userOwnerId != null) {
15
- contribution.userOwnerId = userOwnerId;
13
+ if (requiresActorScope && userId != null) {
14
+ contribution.userId = userId;
16
15
  }
17
16
 
18
17
  return contribution;
@@ -31,10 +30,10 @@ function createWorkspaceRouteVisibilityResolver({ workspaceService } = {}) {
31
30
  }
32
31
 
33
32
  const actor = context?.actor || request?.user || null;
34
- const userOwnerId = normalizeOpaqueId(actor?.id);
33
+ const userId = normalizeOpaqueId(actor?.id);
35
34
  const workspace =
36
35
  context?.workspace || context?.requestMeta?.resolvedWorkspaceContext?.workspace || request?.workspace || null;
37
- const scopeOwnerId = parsePositiveInteger(workspace?.id);
36
+ const scopeOwnerId = normalizeRecordId(workspace?.id, { fallback: null });
38
37
  if (!scopeOwnerId) {
39
38
  const workspaceSlug = normalizeText(input?.workspaceSlug).toLowerCase();
40
39
 
@@ -42,7 +41,7 @@ function createWorkspaceRouteVisibilityResolver({ workspaceService } = {}) {
42
41
  return visibility === "workspace_user"
43
42
  ? buildVisibilityContribution({
44
43
  visibility,
45
- userOwnerId
44
+ userId
46
45
  })
47
46
  : {};
48
47
  }
@@ -50,12 +49,12 @@ function createWorkspaceRouteVisibilityResolver({ workspaceService } = {}) {
50
49
  const resolvedWorkspaceContext = await workspaceService.resolveWorkspaceContextForUserBySlug(actor, workspaceSlug, {
51
50
  request
52
51
  });
53
- const resolvedWorkspaceOwnerId = parsePositiveInteger(resolvedWorkspaceContext?.workspace?.id);
52
+ const resolvedWorkspaceOwnerId = normalizeRecordId(resolvedWorkspaceContext?.workspace?.id, { fallback: null });
54
53
  if (!resolvedWorkspaceOwnerId) {
55
54
  return visibility === "workspace_user"
56
55
  ? buildVisibilityContribution({
57
56
  visibility,
58
- userOwnerId
57
+ userId
59
58
  })
60
59
  : {};
61
60
  }
@@ -63,14 +62,14 @@ function createWorkspaceRouteVisibilityResolver({ workspaceService } = {}) {
63
62
  return buildVisibilityContribution({
64
63
  visibility,
65
64
  scopeOwnerId: resolvedWorkspaceOwnerId,
66
- userOwnerId
65
+ userId
67
66
  });
68
67
  }
69
68
 
70
69
  return buildVisibilityContribution({
71
70
  visibility,
72
71
  scopeOwnerId,
73
- userOwnerId
72
+ userId
74
73
  });
75
74
  }
76
75
  });
@@ -1,9 +1,10 @@
1
1
  import { resolveWorkspaceThemePalettes } from "../../../shared/settings.js";
2
2
  import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/actions/textNormalization";
3
+ import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
3
4
 
4
5
  function mapWorkspaceSummary(workspace, membership) {
5
6
  return {
6
- id: Number(workspace.id),
7
+ id: normalizeRecordId(workspace.id, { fallback: "" }),
7
8
  slug: normalizeText(workspace.slug),
8
9
  name: normalizeText(workspace.name),
9
10
  avatarUrl: normalizeText(workspace.avatarUrl),
@@ -39,7 +40,7 @@ function mapMembershipSummary(membership, workspace) {
39
40
  }
40
41
 
41
42
  return {
42
- workspaceId: Number(workspace?.id || membership.workspaceId),
43
+ workspaceId: normalizeRecordId(workspace?.id || membership.workspaceId, { fallback: "" }),
43
44
  roleSid: normalizeLowerText(membership.roleSid || "member") || "member",
44
45
  status: normalizeLowerText(membership.status || "active") || "active"
45
46
  };
@@ -1,6 +1,12 @@
1
- import { toInsertDateTime, toNullableDateTime, toIsoString } from "@jskit-ai/database-runtime/shared";
1
+ import {
2
+ normalizeDbRecordId,
3
+ toInsertDateTime,
4
+ toNullableDateTime,
5
+ toIsoString,
6
+ createWithTransaction
7
+ } from "@jskit-ai/database-runtime/shared";
2
8
  import { isDuplicateEntryError } from "@jskit-ai/database-runtime/shared/duplicateEntry";
3
- import { normalizeText, normalizeLowerText } from "@jskit-ai/kernel/shared/support/normalize";
9
+ import { normalizeLowerText, normalizeRecordId, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
4
10
 
5
11
  function nowDb() {
6
12
  return toInsertDateTime();
@@ -42,9 +48,12 @@ export {
42
48
  isDuplicateEntryError,
43
49
  normalizeText,
44
50
  normalizeLowerText,
51
+ normalizeRecordId,
52
+ normalizeDbRecordId,
45
53
  nowDb,
46
54
  toNullableIso,
47
55
  uniqueSorted,
48
56
  parseJson,
49
- toDbJson
57
+ toDbJson,
58
+ createWithTransaction
50
59
  };
@@ -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 "./repositoryUtils.js";
6
9
  import { DEFAULT_USER_SETTINGS } from "../../../shared/settings.js";
7
10
  import {
@@ -14,7 +17,7 @@ function mapRow(row) {
14
17
  }
15
18
 
16
19
  const mapped = {
17
- userId: Number(row.user_id),
20
+ userId: normalizeDbRecordId(row.user_id, { fallback: "" }),
18
21
  passwordSignInEnabled: row.password_sign_in_enabled == null ? true : Boolean(row.password_sign_in_enabled),
19
22
  passwordSetupRequired: row.password_setup_required == null ? false : Boolean(row.password_setup_required),
20
23
  createdAt: toIsoString(row.created_at),
@@ -45,8 +48,13 @@ function normalizeBoolean(value, fallback = false) {
45
48
  }
46
49
 
47
50
  function createInsertPayload(userId) {
51
+ const normalizedUserId = normalizeRecordId(userId, { fallback: null });
52
+ if (!normalizedUserId) {
53
+ throw new TypeError("userSettingsRepository requires a valid user id.");
54
+ }
55
+
48
56
  const payload = {
49
- user_id: Number(userId),
57
+ user_id: normalizedUserId,
50
58
  password_sign_in_enabled: DEFAULT_USER_SETTINGS.passwordSignInEnabled,
51
59
  password_setup_required: DEFAULT_USER_SETTINGS.passwordSetupRequired,
52
60
  created_at: nowDb(),
@@ -74,35 +82,50 @@ function createRepository(knex) {
74
82
  if (typeof knex !== "function") {
75
83
  throw new TypeError("userSettingsRepository requires knex.");
76
84
  }
85
+ const withTransaction = createWithTransaction(knex);
77
86
 
78
87
  async function findByUserId(userId, options = {}) {
79
88
  const client = options?.trx || knex;
80
- const row = await client("user_settings").where({ user_id: Number(userId) }).first();
89
+ const normalizedUserId = normalizeRecordId(userId, { fallback: null });
90
+ if (!normalizedUserId) {
91
+ return null;
92
+ }
93
+
94
+ const row = await client("user_settings").where({ user_id: normalizedUserId }).first();
81
95
  return mapRow(row);
82
96
  }
83
97
 
84
98
  async function ensureForUserId(userId, options = {}) {
85
99
  const client = options?.trx || knex;
86
- const numericUserId = Number(userId);
87
- const existing = await findByUserId(numericUserId, { trx: client });
100
+ const normalizedUserId = normalizeRecordId(userId, { fallback: null });
101
+ if (!normalizedUserId) {
102
+ throw new TypeError("userSettingsRepository.ensureForUserId requires a valid user id.");
103
+ }
104
+
105
+ const existing = await findByUserId(normalizedUserId, { trx: client });
88
106
  if (existing) {
89
107
  return existing;
90
108
  }
91
109
 
92
110
  try {
93
- await client("user_settings").insert(createInsertPayload(numericUserId));
111
+ await client("user_settings").insert(createInsertPayload(normalizedUserId));
94
112
  } catch (error) {
95
113
  if (!isDuplicateEntryError(error)) {
96
114
  throw error;
97
115
  }
98
116
  }
99
117
 
100
- return findByUserId(numericUserId, { trx: client });
118
+ return findByUserId(normalizedUserId, { trx: client });
101
119
  }
102
120
 
103
121
  async function patchUserSettings(userId, patch = {}, options = {}) {
104
122
  const client = options?.trx || knex;
105
- const ensured = await ensureForUserId(userId, { trx: client });
123
+ const normalizedUserId = normalizeRecordId(userId, { fallback: null });
124
+ if (!normalizedUserId) {
125
+ throw new TypeError("userSettingsRepository.patchUserSettings requires a valid user id.");
126
+ }
127
+
128
+ const ensured = await ensureForUserId(normalizedUserId, { trx: client });
106
129
  const source = patch && typeof patch === "object" ? patch : {};
107
130
 
108
131
  const dbPatch = {
@@ -125,8 +148,8 @@ function createRepository(knex) {
125
148
  if (Object.hasOwn(source, "passwordSetupRequired")) {
126
149
  dbPatch.password_setup_required = normalizeBoolean(source.passwordSetupRequired, ensured.passwordSetupRequired);
127
150
  }
128
- await client("user_settings").where({ user_id: Number(userId) }).update(dbPatch);
129
- return findByUserId(userId, { trx: client });
151
+ await client("user_settings").where({ user_id: normalizedUserId }).update(dbPatch);
152
+ return findByUserId(normalizedUserId, { trx: client });
130
153
  }
131
154
 
132
155
  async function updatePreferences(userId, patch = {}, options = {}) {
@@ -155,6 +178,7 @@ function createRepository(knex) {
155
178
  }
156
179
 
157
180
  return Object.freeze({
181
+ withTransaction,
158
182
  findByUserId,
159
183
  ensureForUserId,
160
184
  patchUserSettings,
@@ -1,11 +1,14 @@
1
1
  import {
2
2
  isDuplicateEntryError,
3
3
  normalizeLowerText,
4
+ normalizeDbRecordId,
5
+ normalizeRecordId,
4
6
  normalizeText,
5
7
  toIsoString,
6
8
  toNullableDateTime,
7
9
  toNullableIso,
8
- nowDb
10
+ nowDb,
11
+ createWithTransaction
9
12
  } from "./repositoryUtils.js";
10
13
 
11
14
  const USERNAME_MAX_LENGTH = 120;
@@ -55,7 +58,7 @@ function mapProfileRow(row) {
55
58
  return null;
56
59
  }
57
60
  return {
58
- id: Number(row.id),
61
+ id: normalizeDbRecordId(row.id, { fallback: "" }),
59
62
  authProvider: normalizeLowerText(row.auth_provider),
60
63
  authProviderUserSid: normalizeText(row.auth_provider_user_sid),
61
64
  email: normalizeLowerText(row.email),
@@ -90,11 +93,13 @@ function createDuplicateEmailConflictError() {
90
93
  return error;
91
94
  }
92
95
 
93
- async function resolveUniqueUsername(client, baseUsername, { excludeUserId = 0 } = {}) {
96
+ async function resolveUniqueUsername(client, baseUsername, { excludeUserId = null } = {}) {
97
+ const normalizedExcludeUserId = normalizeDbRecordId(excludeUserId, { fallback: null });
94
98
  for (let suffix = 0; suffix < 1000; suffix += 1) {
95
99
  const candidate = buildUsernameCandidate(baseUsername, suffix);
96
100
  const existing = await client("users").where({ username: candidate }).first();
97
- if (!existing || Number(existing.id) === Number(excludeUserId || 0)) {
101
+ const existingId = normalizeDbRecordId(existing?.id, { fallback: null });
102
+ if (!existing || existingId === normalizedExcludeUserId) {
98
103
  return candidate;
99
104
  }
100
105
  }
@@ -106,10 +111,16 @@ function createRepository(knex) {
106
111
  if (typeof knex !== "function") {
107
112
  throw new TypeError("usersRepository requires knex.");
108
113
  }
114
+ const withTransaction = createWithTransaction(knex);
109
115
 
110
116
  async function findById(userId, options = {}) {
117
+ const normalizedUserId = normalizeRecordId(userId, { fallback: null });
118
+ if (!normalizedUserId) {
119
+ return null;
120
+ }
121
+
111
122
  const client = options?.trx || knex;
112
- const row = await client("users").where({ id: userId }).first();
123
+ const row = await client("users").where({ id: normalizedUserId }).first();
113
124
  return mapProfileRow(row);
114
125
  }
115
126
 
@@ -130,42 +141,56 @@ function createRepository(knex) {
130
141
  }
131
142
 
132
143
  async function updateDisplayNameById(userId, displayName, options = {}) {
144
+ const normalizedUserId = normalizeRecordId(userId, { fallback: null });
145
+ if (!normalizedUserId) {
146
+ return null;
147
+ }
148
+
133
149
  const client = options?.trx || knex;
134
150
  await client("users")
135
- .where({ id: userId })
151
+ .where({ id: normalizedUserId })
136
152
  .update({
137
153
  display_name: normalizeText(displayName)
138
154
  });
139
- return findById(userId, { trx: client });
155
+ return findById(normalizedUserId, { trx: client });
140
156
  }
141
157
 
142
158
  async function updateAvatarById(userId, avatar = {}, options = {}) {
159
+ const normalizedUserId = normalizeRecordId(userId, { fallback: null });
160
+ if (!normalizedUserId) {
161
+ return null;
162
+ }
163
+
143
164
  const client = options?.trx || knex;
144
165
  await client("users")
145
- .where({ id: userId })
166
+ .where({ id: normalizedUserId })
146
167
  .update({
147
168
  avatar_storage_key: avatar.avatarStorageKey || null,
148
169
  avatar_version: avatar.avatarVersion == null ? null : String(avatar.avatarVersion),
149
170
  avatar_updated_at: toNullableDateTime(avatar.avatarUpdatedAt) || nowDb()
150
171
  });
151
172
 
152
- return findById(userId, { trx: client });
173
+ return findById(normalizedUserId, { trx: client });
153
174
  }
154
175
 
155
176
  async function clearAvatarById(userId, options = {}) {
177
+ const normalizedUserId = normalizeRecordId(userId, { fallback: null });
178
+ if (!normalizedUserId) {
179
+ return null;
180
+ }
181
+
156
182
  const client = options?.trx || knex;
157
183
  await client("users")
158
- .where({ id: userId })
184
+ .where({ id: normalizedUserId })
159
185
  .update({
160
186
  avatar_storage_key: null,
161
187
  avatar_version: null,
162
188
  avatar_updated_at: null
163
189
  });
164
- return findById(userId, { trx: client });
190
+ return findById(normalizedUserId, { trx: client });
165
191
  }
166
192
 
167
193
  async function upsert(profileLike = {}, options = {}) {
168
- const client = options?.trx || knex;
169
194
  const identity = normalizeIdentity(profileLike);
170
195
  if (!identity) {
171
196
  throw new TypeError("upsert requires provider/authProvider and providerUserId/authProviderUserSid.");
@@ -191,7 +216,7 @@ function createRepository(knex) {
191
216
  const username = existingUsername || (await resolveUniqueUsername(trx, requestedUsername || usernameBaseFromEmail(email), {
192
217
  excludeUserId: existing.id
193
218
  }));
194
- await trx("users").where({ id: existing.id }).update({
219
+ await trx("users").where({ id: normalizeDbRecordId(existing.id, { fallback: null }) }).update({
195
220
  email,
196
221
  display_name: displayName,
197
222
  username
@@ -218,34 +243,26 @@ function createRepository(knex) {
218
243
  }
219
244
  }
220
245
 
221
- const reloaded = await trx("users").where(where).first();
222
- return mapProfileRow(reloaded);
246
+ const resolved = await trx("users").where(where).first();
247
+ return mapProfileRow(resolved);
223
248
  };
224
249
 
225
250
  if (options?.trx) {
226
- return executeUpsert(client);
251
+ return executeUpsert(options.trx);
227
252
  }
228
253
 
229
254
  return knex.transaction(executeUpsert);
230
255
  }
231
256
 
232
- async function withTransaction(work) {
233
- if (typeof work !== "function") {
234
- throw new TypeError("withTransaction requires a callback.");
235
- }
236
-
237
- return knex.transaction((trx) => work(trx));
238
- }
239
-
240
257
  return Object.freeze({
258
+ withTransaction,
241
259
  findById,
242
260
  findByIdentity,
243
261
  updateDisplayNameById,
244
262
  updateAvatarById,
245
263
  clearAvatarById,
246
- upsert,
247
- withTransaction
264
+ upsert
248
265
  });
249
266
  }
250
267
 
251
- export { createRepository, normalizeIdentity, mapProfileRow };
268
+ export { createRepository, mapProfileRow, normalizeIdentity };
@@ -1,11 +1,15 @@
1
+ import { resolveInsertedRecordId } from "@jskit-ai/database-runtime/shared";
1
2
  import {
2
3
  normalizeLowerText,
4
+ normalizeDbRecordId,
5
+ normalizeRecordId,
3
6
  normalizeText,
4
7
  toIsoString,
5
8
  toNullableIso,
6
9
  toNullableDateTime,
7
10
  nowDb,
8
- isDuplicateEntryError
11
+ isDuplicateEntryError,
12
+ createWithTransaction
9
13
  } from "./repositoryUtils.js";
10
14
 
11
15
  function mapRow(row) {
@@ -14,13 +18,13 @@ function mapRow(row) {
14
18
  }
15
19
 
16
20
  return {
17
- id: Number(row.id),
18
- workspaceId: Number(row.workspace_id),
21
+ id: normalizeDbRecordId(row.id, { fallback: "" }),
22
+ workspaceId: normalizeDbRecordId(row.workspace_id, { fallback: "" }),
19
23
  email: normalizeLowerText(row.email),
20
24
  roleSid: normalizeLowerText(row.role_sid || "member") || "member",
21
25
  status: normalizeLowerText(row.status || "pending") || "pending",
22
26
  tokenHash: normalizeText(row.token_hash),
23
- invitedByUserId: row.invited_by_user_id == null ? null : Number(row.invited_by_user_id),
27
+ invitedByUserId: row.invited_by_user_id == null ? null : normalizeDbRecordId(row.invited_by_user_id, { fallback: null }),
24
28
  expiresAt: toNullableIso(row.expires_at),
25
29
  acceptedAt: toNullableIso(row.accepted_at),
26
30
  revokedAt: toNullableIso(row.revoked_at),
@@ -43,6 +47,7 @@ function createRepository(knex) {
43
47
  if (typeof knex !== "function") {
44
48
  throw new TypeError("workspaceInvitesRepository requires knex.");
45
49
  }
50
+ const withTransaction = createWithTransaction(knex);
46
51
 
47
52
  async function findPendingByTokenHash(tokenHash, options = {}) {
48
53
  const client = options?.trx || knex;
@@ -69,10 +74,15 @@ function createRepository(knex) {
69
74
  }
70
75
 
71
76
  async function listPendingByWorkspaceIdWithWorkspace(workspaceId, options = {}) {
77
+ const normalizedWorkspaceId = normalizeRecordId(workspaceId, { fallback: null });
78
+ if (!normalizedWorkspaceId) {
79
+ return [];
80
+ }
81
+
72
82
  const client = options?.trx || knex;
73
83
  const rows = await client("workspace_invites as wi")
74
84
  .join("workspaces as w", "w.id", "wi.workspace_id")
75
- .where({ "wi.workspace_id": Number(workspaceId), "wi.status": "pending" })
85
+ .where({ "wi.workspace_id": normalizedWorkspaceId, "wi.status": "pending" })
76
86
  .orderBy("wi.created_at", "desc")
77
87
  .select(WORKSPACE_INVITE_WITH_WORKSPACE_SELECT);
78
88
 
@@ -82,14 +92,18 @@ function createRepository(knex) {
82
92
  async function insert(payload = {}, options = {}) {
83
93
  const client = options?.trx || knex;
84
94
  const source = payload && typeof payload === "object" ? payload : {};
95
+ const workspaceId = normalizeRecordId(source.workspaceId, { fallback: null });
96
+ if (!workspaceId) {
97
+ throw new TypeError("workspaceInvitesRepository.insert requires workspaceId.");
98
+ }
85
99
 
86
100
  const insertPayload = {
87
- workspace_id: Number(source.workspaceId),
101
+ workspace_id: workspaceId,
88
102
  email: normalizeLowerText(source.email),
89
103
  role_sid: normalizeLowerText(source.roleSid || "member") || "member",
90
104
  status: normalizeLowerText(source.status || "pending") || "pending",
91
105
  token_hash: normalizeText(source.tokenHash),
92
- invited_by_user_id: source.invitedByUserId == null ? null : Number(source.invitedByUserId),
106
+ invited_by_user_id: source.invitedByUserId == null ? null : normalizeRecordId(source.invitedByUserId, { fallback: null }),
93
107
  expires_at: toNullableDateTime(source.expiresAt),
94
108
  accepted_at: null,
95
109
  revoked_at: null,
@@ -99,8 +113,8 @@ function createRepository(knex) {
99
113
 
100
114
  try {
101
115
  const result = await client("workspace_invites").insert(insertPayload);
102
- const insertedId = Array.isArray(result) ? Number(result[0]) : Number(result);
103
- if (Number.isInteger(insertedId) && insertedId > 0) {
116
+ const insertedId = resolveInsertedRecordId(result, { fallback: null });
117
+ if (insertedId) {
104
118
  const row = await client("workspace_invites").where({ id: insertedId }).first();
105
119
  return mapRow(row);
106
120
  }
@@ -118,9 +132,14 @@ function createRepository(knex) {
118
132
  }
119
133
 
120
134
  async function expirePendingByWorkspaceIdAndEmail(workspaceId, email, options = {}) {
135
+ const normalizedWorkspaceId = normalizeRecordId(workspaceId, { fallback: null });
136
+ if (!normalizedWorkspaceId) {
137
+ return;
138
+ }
139
+
121
140
  const client = options?.trx || knex;
122
141
  await client("workspace_invites")
123
- .where({ workspace_id: Number(workspaceId), email: normalizeLowerText(email), status: "pending" })
142
+ .where({ workspace_id: normalizedWorkspaceId, email: normalizeLowerText(email), status: "pending" })
124
143
  .update({
125
144
  status: "expired",
126
145
  updated_at: nowDb()
@@ -128,9 +147,14 @@ function createRepository(knex) {
128
147
  }
129
148
 
130
149
  async function markAcceptedById(inviteId, options = {}) {
150
+ const normalizedInviteId = normalizeRecordId(inviteId, { fallback: null });
151
+ if (!normalizedInviteId) {
152
+ return;
153
+ }
154
+
131
155
  const client = options?.trx || knex;
132
156
  await client("workspace_invites")
133
- .where({ id: Number(inviteId) })
157
+ .where({ id: normalizedInviteId })
134
158
  .update({
135
159
  status: "accepted",
136
160
  accepted_at: nowDb(),
@@ -139,9 +163,14 @@ function createRepository(knex) {
139
163
  }
140
164
 
141
165
  async function revokeById(inviteId, options = {}) {
166
+ const normalizedInviteId = normalizeRecordId(inviteId, { fallback: null });
167
+ if (!normalizedInviteId) {
168
+ return;
169
+ }
170
+
142
171
  const client = options?.trx || knex;
143
172
  await client("workspace_invites")
144
- .where({ id: Number(inviteId) })
173
+ .where({ id: normalizedInviteId })
145
174
  .update({
146
175
  status: "revoked",
147
176
  revoked_at: nowDb(),
@@ -150,14 +179,21 @@ function createRepository(knex) {
150
179
  }
151
180
 
152
181
  async function findPendingByIdForWorkspace(inviteId, workspaceId, options = {}) {
182
+ const normalizedInviteId = normalizeRecordId(inviteId, { fallback: null });
183
+ const normalizedWorkspaceId = normalizeRecordId(workspaceId, { fallback: null });
184
+ if (!normalizedInviteId || !normalizedWorkspaceId) {
185
+ return null;
186
+ }
187
+
153
188
  const client = options?.trx || knex;
154
189
  const row = await client("workspace_invites")
155
- .where({ id: Number(inviteId), workspace_id: Number(workspaceId), status: "pending" })
190
+ .where({ id: normalizedInviteId, workspace_id: normalizedWorkspaceId, status: "pending" })
156
191
  .first();
157
192
  return mapRow(row);
158
193
  }
159
194
 
160
195
  return Object.freeze({
196
+ withTransaction,
161
197
  findPendingByTokenHash,
162
198
  listPendingByEmail,
163
199
  listPendingByWorkspaceIdWithWorkspace,