@jskit-ai/users-core 0.1.65 → 0.1.67

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 (56) hide show
  1. package/package.descriptor.mjs +14 -65
  2. package/package.json +10 -10
  3. package/src/server/UsersCoreServiceProvider.js +18 -2
  4. package/src/server/accountNotifications/accountNotificationsActions.js +3 -5
  5. package/src/server/accountNotifications/accountNotificationsService.js +3 -2
  6. package/src/server/accountNotifications/bootAccountNotificationsRoutes.js +12 -11
  7. package/src/server/accountPreferences/accountPreferencesActions.js +3 -5
  8. package/src/server/accountPreferences/accountPreferencesService.js +3 -2
  9. package/src/server/accountPreferences/bootAccountPreferencesRoutes.js +12 -11
  10. package/src/server/accountProfile/accountProfileActions.js +15 -32
  11. package/src/server/accountProfile/accountProfileService.js +9 -8
  12. package/src/server/accountProfile/bootAccountProfileRoutes.js +25 -19
  13. package/src/server/accountSecurity/accountSecurityActions.js +21 -16
  14. package/src/server/accountSecurity/accountSecurityService.js +16 -6
  15. package/src/server/accountSecurity/bootAccountSecurityRoutes.js +52 -40
  16. package/src/server/common/formatters/accountSettingsResponseFormatter.js +8 -18
  17. package/src/server/common/registerCommonRepositories.js +5 -2
  18. package/src/server/common/repositories/userProfilesRepository.js +227 -88
  19. package/src/server/common/repositories/userSettingsRepository.js +108 -100
  20. package/src/server/common/support/accountSettingsJsonApiTransport.js +10 -0
  21. package/src/server/usersBootstrapContributor.js +13 -32
  22. package/src/shared/resources/accountSettingsSchemas.js +83 -0
  23. package/src/shared/resources/userProfileResource.js +146 -126
  24. package/src/shared/resources/userSettingsResource.js +376 -353
  25. package/templates/packages/users/package.descriptor.mjs +4 -5
  26. package/templates/packages/users/package.json +0 -1
  27. package/templates/packages/users/src/server/UsersProvider.js +23 -24
  28. package/templates/packages/users/src/server/actions.js +26 -28
  29. package/templates/packages/users/src/server/registerRoutes.js +29 -15
  30. package/templates/packages/users/src/server/repository.js +35 -28
  31. package/templates/packages/users/src/server/service.js +20 -15
  32. package/templates/packages/users/src/shared/userResource.js +55 -68
  33. package/templates/packages/users-workspace/package.descriptor.mjs +4 -5
  34. package/templates/packages/users-workspace/src/server/UsersProvider.js +23 -24
  35. package/templates/packages/users-workspace/src/server/actions.js +28 -28
  36. package/templates/packages/users-workspace/src/server/registerRoutes.js +34 -16
  37. package/test/accountSecurityService.test.js +32 -0
  38. package/test/providerLifecycle.test.js +63 -0
  39. package/test/registerCommonRepositories.test.js +28 -8
  40. package/test/repositoryContracts.test.js +177 -28
  41. package/test/resourcesCanonical.test.js +18 -11
  42. package/test/userSettingsInternalResource.test.js +8 -0
  43. package/test/userSettingsResource.test.js +24 -7
  44. package/test/usersBootstrapContributor.test.js +40 -1
  45. package/test/usersPackageScaffoldContract.test.js +82 -4
  46. package/test/usersRouteRequestInputValidator.test.js +92 -23
  47. package/test/usersRouteResources.test.js +28 -18
  48. package/src/server/common/resources/userProfilesResource.js +0 -203
  49. package/src/server/common/validators/authenticatedUserValidator.js +0 -43
  50. package/src/shared/resources/resolveGlobalArrayRegistry.js +0 -6
  51. package/src/shared/resources/userSettingsFields.js +0 -76
  52. package/templates/packages/main/src/shared/resources/userSettingsFields.js +0 -138
  53. package/templates/packages/users/src/server/actionIds.js +0 -6
  54. package/templates/packages/users/src/server/listConfig.js +0 -16
  55. package/test/settingsFieldRegistriesSingleton.test.js +0 -14
  56. package/test-support/registerDefaultSettingsFields.js +0 -2
@@ -1,29 +1,97 @@
1
- import { createCrudResourceRuntime } from "@jskit-ai/crud-core/server/resourceRuntime";
2
1
  import {
2
+ createWithTransaction,
3
3
  isDuplicateEntryError,
4
4
  normalizeDbRecordId,
5
5
  normalizeLowerText,
6
6
  normalizeRecordId,
7
- nowDb
7
+ normalizeText,
8
+ nowDb,
9
+ toIsoString
8
10
  } from "./repositoryUtils.js";
11
+ import {
12
+ createJsonApiInputRecord,
13
+ createJsonRestContext,
14
+ simplifyJsonApiDocument
15
+ } from "@jskit-ai/json-rest-api-core/server/jsonRestApiHost";
9
16
  import { normalizeIdentity } from "../support/identity.js";
10
- import { resource } from "../resources/userProfilesResource.js";
11
17
 
18
+ const RESOURCE_TYPE = "userProfiles";
12
19
  const USERNAME_MAX_LENGTH = 120;
13
- const REPOSITORY_CONFIG = Object.freeze({
14
- context: "internal.repository.user-profiles"
15
- });
16
20
 
17
- function normalizeProfileRecord(payload) {
18
- return payload ? resource.operations.view.outputValidator.normalize(payload) : null;
21
+ function normalizeUsername(value) {
22
+ const normalized = normalizeLowerText(value)
23
+ .replace(/[^a-z0-9]+/g, "-")
24
+ .replace(/^-+|-+$/g, "")
25
+ .slice(0, USERNAME_MAX_LENGTH);
26
+
27
+ return normalized || "";
19
28
  }
20
29
 
21
- function normalizeCreatePayload(payload = {}) {
22
- return resource.operations.create.bodyValidator.normalize(payload);
30
+ function normalizeNullableString(value) {
31
+ if (value === null || value === undefined) {
32
+ return null;
33
+ }
34
+
35
+ return normalizeText(value);
23
36
  }
24
37
 
25
- function normalizeUsername(value) {
26
- return normalizeCreatePayload({ username: value }).username || "";
38
+ function normalizeNullableVersion(value) {
39
+ if (value === null || value === undefined || value === "") {
40
+ return null;
41
+ }
42
+
43
+ return String(value);
44
+ }
45
+
46
+ function normalizeProfileRecord(payload = null) {
47
+ if (!payload || typeof payload !== "object") {
48
+ return null;
49
+ }
50
+
51
+ return {
52
+ id: normalizeDbRecordId(payload.id, { fallback: null }),
53
+ authProvider: normalizeLowerText(payload.authProvider),
54
+ authProviderUserSid: normalizeText(payload.authProviderUserSid),
55
+ email: normalizeLowerText(payload.email),
56
+ username: normalizeUsername(payload.username),
57
+ displayName: normalizeText(payload.displayName),
58
+ avatarStorageKey: normalizeNullableString(payload.avatarStorageKey),
59
+ avatarVersion: normalizeNullableVersion(payload.avatarVersion),
60
+ avatarUpdatedAt: payload.avatarUpdatedAt ? toIsoString(payload.avatarUpdatedAt) : null,
61
+ createdAt: payload.createdAt ? toIsoString(payload.createdAt) : null
62
+ };
63
+ }
64
+
65
+ function normalizeCreatePayload(payload = {}) {
66
+ const source = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
67
+ const normalized = {};
68
+
69
+ if (Object.hasOwn(source, "authProvider") || Object.hasOwn(source, "provider")) {
70
+ normalized.authProvider = normalizeLowerText(source.authProvider ?? source.provider);
71
+ }
72
+ if (Object.hasOwn(source, "authProviderUserSid") || Object.hasOwn(source, "providerUserId")) {
73
+ normalized.authProviderUserSid = normalizeText(source.authProviderUserSid ?? source.providerUserId);
74
+ }
75
+ if (Object.hasOwn(source, "email")) {
76
+ normalized.email = normalizeLowerText(source.email);
77
+ }
78
+ if (Object.hasOwn(source, "username")) {
79
+ normalized.username = normalizeUsername(source.username);
80
+ }
81
+ if (Object.hasOwn(source, "displayName")) {
82
+ normalized.displayName = normalizeText(source.displayName);
83
+ }
84
+ if (Object.hasOwn(source, "avatarStorageKey")) {
85
+ normalized.avatarStorageKey = normalizeNullableString(source.avatarStorageKey);
86
+ }
87
+ if (Object.hasOwn(source, "avatarVersion")) {
88
+ normalized.avatarVersion = normalizeNullableVersion(source.avatarVersion);
89
+ }
90
+ if (Object.hasOwn(source, "avatarUpdatedAt")) {
91
+ normalized.avatarUpdatedAt = source.avatarUpdatedAt == null ? null : new Date(source.avatarUpdatedAt);
92
+ }
93
+
94
+ return normalized;
27
95
  }
28
96
 
29
97
  function usernameBaseFromEmail(email) {
@@ -69,11 +137,23 @@ function createDuplicateEmailConflictError() {
69
137
  return error;
70
138
  }
71
139
 
72
- async function resolveUniqueUsername(client, baseUsername, { excludeUserId = null } = {}) {
140
+ async function resolveUniqueUsername(api, baseUsername, { excludeUserId = null, transaction = null } = {}) {
73
141
  const normalizedExcludeUserId = normalizeDbRecordId(excludeUserId, { fallback: null });
142
+
74
143
  for (let suffix = 0; suffix < 1000; suffix += 1) {
75
144
  const candidate = buildUsernameCandidate(baseUsername, suffix);
76
- const existing = await client("users").where({ username: candidate }).first();
145
+ const result = await api.resources.userProfiles.query({
146
+ queryParams: {
147
+ filters: {
148
+ username: candidate
149
+ }
150
+ },
151
+ transaction,
152
+ simplified: false
153
+ });
154
+
155
+ const existingRows = simplifyJsonApiDocument(result);
156
+ const existing = Array.isArray(existingRows) ? existingRows[0] || null : null;
77
157
  const existingId = normalizeDbRecordId(existing?.id, { fallback: null });
78
158
  if (!existing || existingId === normalizedExcludeUserId) {
79
159
  return candidate;
@@ -83,13 +163,31 @@ async function resolveUniqueUsername(client, baseUsername, { excludeUserId = nul
83
163
  throw new Error("Unable to generate unique username.");
84
164
  }
85
165
 
86
- function createRepository(knex) {
166
+ function createRepository({ api, knex } = {}) {
167
+ if (!api?.resources?.userProfiles) {
168
+ throw new TypeError("internal.repository.user-profiles requires json-rest-api userProfiles resource.");
169
+ }
87
170
  if (typeof knex !== "function") {
88
171
  throw new TypeError("internal.repository.user-profiles requires knex.");
89
172
  }
90
173
 
91
- const resourceRuntime = createCrudResourceRuntime(resource, knex, REPOSITORY_CONFIG);
92
- const withTransaction = resourceRuntime.withTransaction;
174
+ const withTransaction = createWithTransaction(knex);
175
+
176
+ async function queryFirst(filters = {}, options = {}) {
177
+ const result = await api.resources.userProfiles.query(
178
+ {
179
+ queryParams: {
180
+ filters
181
+ },
182
+ transaction: options?.trx || null,
183
+ simplified: false
184
+ },
185
+ createJsonRestContext(options?.context || null)
186
+ );
187
+
188
+ const rows = simplifyJsonApiDocument(result);
189
+ return Array.isArray(rows) ? rows[0] || null : null;
190
+ }
93
191
 
94
192
  async function findById(userId, options = {}) {
95
193
  const normalizedUserId = normalizeRecordId(userId, { fallback: null });
@@ -97,10 +195,7 @@ function createRepository(knex) {
97
195
  return null;
98
196
  }
99
197
 
100
- return resourceRuntime.findById(normalizedUserId, {
101
- ...options,
102
- include: "none"
103
- });
198
+ return normalizeProfileRecord(await queryFirst({ id: normalizedUserId }, options));
104
199
  }
105
200
 
106
201
  async function findByEmail(email, options = {}) {
@@ -109,9 +204,7 @@ function createRepository(knex) {
109
204
  return null;
110
205
  }
111
206
 
112
- const client = options?.trx || knex;
113
- const row = await client("users").where({ email: normalizedEmail }).first();
114
- return row ? normalizeProfileRecord(row) : null;
207
+ return normalizeProfileRecord(await queryFirst({ email: normalizedEmail }, options));
115
208
  }
116
209
 
117
210
  async function findByIdentity(identityLike, options = {}) {
@@ -120,15 +213,15 @@ function createRepository(knex) {
120
213
  return null;
121
214
  }
122
215
 
123
- const client = options?.trx || knex;
124
- const row = await client("users")
125
- .where({
126
- auth_provider: identity.provider,
127
- auth_provider_user_sid: identity.providerUserId
128
- })
129
- .first();
130
-
131
- return row ? normalizeProfileRecord(row) : null;
216
+ return normalizeProfileRecord(
217
+ await queryFirst(
218
+ {
219
+ authProvider: identity.provider,
220
+ authProviderUserSid: identity.providerUserId
221
+ },
222
+ options
223
+ )
224
+ );
132
225
  }
133
226
 
134
227
  async function updateDisplayNameById(userId, displayName, options = {}) {
@@ -137,14 +230,25 @@ function createRepository(knex) {
137
230
  return null;
138
231
  }
139
232
 
140
- return resourceRuntime.updateById(
141
- normalizedUserId,
142
- { displayName },
233
+ const updated = await api.resources.userProfiles.patch(
143
234
  {
144
- ...options,
145
- include: "none"
146
- }
235
+ inputRecord: createJsonApiInputRecord(
236
+ RESOURCE_TYPE,
237
+ {
238
+ displayName,
239
+ updatedAt: new Date()
240
+ },
241
+ {
242
+ id: normalizedUserId
243
+ }
244
+ ),
245
+ transaction: options?.trx || null,
246
+ simplified: false
247
+ },
248
+ createJsonRestContext(options?.context || null)
147
249
  );
250
+
251
+ return normalizeProfileRecord(simplifyJsonApiDocument(updated));
148
252
  }
149
253
 
150
254
  async function updateAvatarById(userId, avatar = {}, options = {}) {
@@ -153,18 +257,27 @@ function createRepository(knex) {
153
257
  return null;
154
258
  }
155
259
 
156
- return resourceRuntime.updateById(
157
- normalizedUserId,
260
+ const updated = await api.resources.userProfiles.patch(
158
261
  {
159
- avatarStorageKey: avatar.avatarStorageKey ?? null,
160
- avatarVersion: avatar.avatarVersion ?? null,
161
- avatarUpdatedAt: avatar.avatarUpdatedAt ?? nowDb()
262
+ inputRecord: createJsonApiInputRecord(
263
+ RESOURCE_TYPE,
264
+ {
265
+ avatarStorageKey: avatar.avatarStorageKey ?? null,
266
+ avatarVersion: avatar.avatarVersion ?? null,
267
+ avatarUpdatedAt: avatar.avatarUpdatedAt ?? nowDb(),
268
+ updatedAt: new Date()
269
+ },
270
+ {
271
+ id: normalizedUserId
272
+ }
273
+ ),
274
+ transaction: options?.trx || null,
275
+ simplified: false
162
276
  },
163
- {
164
- ...options,
165
- include: "none"
166
- }
277
+ createJsonRestContext(options?.context || null)
167
278
  );
279
+
280
+ return normalizeProfileRecord(simplifyJsonApiDocument(updated));
168
281
  }
169
282
 
170
283
  async function clearAvatarById(userId, options = {}) {
@@ -173,18 +286,27 @@ function createRepository(knex) {
173
286
  return null;
174
287
  }
175
288
 
176
- return resourceRuntime.updateById(
177
- normalizedUserId,
289
+ const updated = await api.resources.userProfiles.patch(
178
290
  {
179
- avatarStorageKey: null,
180
- avatarVersion: null,
181
- avatarUpdatedAt: null
291
+ inputRecord: createJsonApiInputRecord(
292
+ RESOURCE_TYPE,
293
+ {
294
+ avatarStorageKey: null,
295
+ avatarVersion: null,
296
+ avatarUpdatedAt: null,
297
+ updatedAt: new Date()
298
+ },
299
+ {
300
+ id: normalizedUserId
301
+ }
302
+ ),
303
+ transaction: options?.trx || null,
304
+ simplified: false
182
305
  },
183
- {
184
- ...options,
185
- include: "none"
186
- }
306
+ createJsonRestContext(options?.context || null)
187
307
  );
308
+
309
+ return normalizeProfileRecord(simplifyJsonApiDocument(updated));
188
310
  }
189
311
 
190
312
  async function upsert(profileLike = {}, options = {}) {
@@ -205,53 +327,68 @@ function createRepository(knex) {
205
327
  }
206
328
 
207
329
  const executeUpsert = async (trx) => {
208
- const where = {
209
- auth_provider: identity.provider,
210
- auth_provider_user_sid: identity.providerUserId
211
- };
212
- const existing = await trx("users").where(where).first();
330
+ const existing = await findByIdentity(identity, {
331
+ trx,
332
+ context: options?.context || null
333
+ });
213
334
 
214
335
  try {
215
336
  if (existing) {
216
337
  const existingUsername = normalizeUsername(existing.username);
217
338
  const username = existingUsername || (
218
339
  await resolveUniqueUsername(
219
- trx,
340
+ api,
220
341
  requestedUsername || usernameBaseFromEmail(email),
221
- { excludeUserId: existing.id }
342
+ { excludeUserId: existing.id, transaction: trx }
222
343
  )
223
344
  );
224
- return resourceRuntime.updateById(
225
- normalizeDbRecordId(existing.id, { fallback: null }),
345
+
346
+ const updated = await api.resources.userProfiles.patch(
226
347
  {
227
- email,
228
- displayName,
229
- username
348
+ inputRecord: createJsonApiInputRecord(
349
+ RESOURCE_TYPE,
350
+ {
351
+ email,
352
+ displayName,
353
+ username,
354
+ updatedAt: new Date()
355
+ },
356
+ {
357
+ id: normalizeDbRecordId(existing.id, { fallback: null })
358
+ }
359
+ ),
360
+ transaction: trx,
361
+ simplified: false
230
362
  },
231
- {
232
- trx,
233
- include: "none"
234
- }
363
+ createJsonRestContext(options?.context || null)
235
364
  );
365
+
366
+ return normalizeProfileRecord(simplifyJsonApiDocument(updated));
236
367
  }
237
368
 
238
369
  const username = await resolveUniqueUsername(
239
- trx,
240
- requestedUsername || usernameBaseFromEmail(email)
370
+ api,
371
+ requestedUsername || usernameBaseFromEmail(email),
372
+ { transaction: trx }
241
373
  );
242
- return resourceRuntime.create(
374
+
375
+ const created = await api.resources.userProfiles.post(
243
376
  {
244
- authProvider: identity.provider,
245
- authProviderUserSid: identity.providerUserId,
246
- email,
247
- displayName,
248
- username
377
+ inputRecord: createJsonApiInputRecord(RESOURCE_TYPE, {
378
+ authProvider: identity.provider,
379
+ authProviderUserSid: identity.providerUserId,
380
+ email,
381
+ displayName,
382
+ username,
383
+ createdAt: new Date()
384
+ }),
385
+ transaction: trx,
386
+ simplified: false
249
387
  },
250
- {
251
- trx,
252
- include: "none"
253
- }
388
+ createJsonRestContext(options?.context || null)
254
389
  );
390
+
391
+ return normalizeProfileRecord(simplifyJsonApiDocument(created));
255
392
  } catch (error) {
256
393
  if (duplicateTargetsEmail(error)) {
257
394
  throw createDuplicateEmailConflictError();
@@ -264,15 +401,17 @@ function createRepository(knex) {
264
401
  }
265
402
  }
266
403
 
267
- const resolved = await trx("users").where(where).first();
268
- return resolved ? normalizeProfileRecord(resolved) : null;
404
+ return findByIdentity(identity, {
405
+ trx,
406
+ context: options?.context || null
407
+ });
269
408
  };
270
409
 
271
410
  if (options?.trx) {
272
411
  return executeUpsert(options.trx);
273
412
  }
274
413
 
275
- return knex.transaction(executeUpsert);
414
+ return withTransaction(executeUpsert);
276
415
  }
277
416
 
278
417
  return Object.freeze({