@jskit-ai/users-core 0.1.142 → 0.1.145

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.
@@ -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.142",
4
+ version: "0.1.145",
5
5
  kind: "runtime",
6
6
  description: "Users/account runtime plus HTTP routes for account features.",
7
7
  dependsOn: [
@@ -146,16 +146,16 @@ export default Object.freeze({
146
146
  mutations: {
147
147
  dependencies: {
148
148
  runtime: {
149
- "@jskit-ai/auth-core": "0.1.129",
150
- "@jskit-ai/crud-core": "0.1.140",
151
- "@jskit-ai/database-runtime": "0.1.131",
152
- "@jskit-ai/http-runtime": "0.1.130",
153
- "@jskit-ai/json-rest-api-core": "0.1.76",
154
- "@jskit-ai/kernel": "0.1.132",
155
- "@jskit-ai/resource-core": "0.1.75",
156
- "@jskit-ai/resource-crud-core": "0.1.75",
149
+ "@jskit-ai/auth-core": "0.1.132",
150
+ "@jskit-ai/crud-core": "0.1.143",
151
+ "@jskit-ai/database-runtime": "0.1.133",
152
+ "@jskit-ai/http-runtime": "0.1.132",
153
+ "@jskit-ai/json-rest-api-core": "0.1.78",
154
+ "@jskit-ai/kernel": "0.1.134",
155
+ "@jskit-ai/resource-core": "0.1.77",
156
+ "@jskit-ai/resource-crud-core": "0.1.77",
157
157
  "@local/users": "file:packages/users",
158
- "@jskit-ai/uploads-runtime": "0.1.108"
158
+ "@jskit-ai/uploads-runtime": "0.1.110"
159
159
  },
160
160
  dev: {}
161
161
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/users-core",
3
- "version": "0.1.142",
3
+ "version": "0.1.145",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -11,14 +11,14 @@
11
11
  "./shared/resources/userSettingsResource": "./src/shared/resources/userSettingsResource.js"
12
12
  },
13
13
  "dependencies": {
14
- "@jskit-ai/auth-core": "0.1.129",
15
- "@jskit-ai/database-runtime": "0.1.131",
16
- "@jskit-ai/http-runtime": "0.1.130",
17
- "@jskit-ai/json-rest-api-core": "0.1.76",
18
- "@jskit-ai/kernel": "0.1.132",
19
- "@jskit-ai/resource-crud-core": "0.1.75",
20
- "@jskit-ai/resource-core": "0.1.75",
21
- "@jskit-ai/uploads-runtime": "0.1.108",
14
+ "@jskit-ai/auth-core": "0.1.132",
15
+ "@jskit-ai/database-runtime": "0.1.133",
16
+ "@jskit-ai/http-runtime": "0.1.132",
17
+ "@jskit-ai/json-rest-api-core": "0.1.78",
18
+ "@jskit-ai/kernel": "0.1.134",
19
+ "@jskit-ai/resource-crud-core": "0.1.77",
20
+ "@jskit-ai/resource-core": "0.1.77",
21
+ "@jskit-ai/uploads-runtime": "0.1.110",
22
22
  "json-rest-schema": "1.x.x"
23
23
  }
24
24
  }
@@ -174,8 +174,8 @@ function createRepository({ api, knex } = {}) {
174
174
 
175
175
  const withTransaction = createWithTransaction(knex);
176
176
 
177
- async function queryFirst(filters = {}, options = {}) {
178
- const rows = extractJsonRestCollectionRows(
177
+ async function queryProfiles(filters = {}, options = {}) {
178
+ return extractJsonRestCollectionRows(
179
179
  await api.resources.userProfiles.query(
180
180
  {
181
181
  queryParams: {
@@ -187,8 +187,10 @@ function createRepository({ api, knex } = {}) {
187
187
  createJsonRestContext(options?.context || null)
188
188
  )
189
189
  );
190
+ }
190
191
 
191
- return rows[0] || null;
192
+ async function queryFirst(filters = {}, options = {}) {
193
+ return (await queryProfiles(filters, options))[0] || null;
192
194
  }
193
195
 
194
196
  async function findById(userId, options = {}) {
@@ -209,6 +211,21 @@ function createRepository({ api, knex } = {}) {
209
211
  return normalizeProfileRecord(await queryFirst({ email: normalizedEmail }, options));
210
212
  }
211
213
 
214
+ async function findByUsername(username, options = {}) {
215
+ const normalizedUsername = normalizeUsername(username);
216
+ if (!normalizedUsername) {
217
+ return null;
218
+ }
219
+
220
+ const rows = await queryProfiles({ username: normalizedUsername }, options);
221
+ if (rows.length > 1) {
222
+ const error = new Error("Application login matches more than one user profile.");
223
+ error.code = "USER_PROFILE_LOGIN_AMBIGUOUS";
224
+ throw error;
225
+ }
226
+ return normalizeProfileRecord(rows[0] || null);
227
+ }
228
+
212
229
  async function findByIdentity(identityLike, options = {}) {
213
230
  const identity = normalizeIdentity(identityLike);
214
231
  if (!identity) {
@@ -416,6 +433,7 @@ function createRepository({ api, knex } = {}) {
416
433
  findById,
417
434
  findByEmail,
418
435
  findByIdentity,
436
+ findByUsername,
419
437
  updateDisplayNameById,
420
438
  updateAvatarById,
421
439
  clearAvatarById,
@@ -68,6 +68,9 @@ function createService({ userProfilesRepository, lifecycleContributors = [], use
68
68
  if (!userProfilesRepository || typeof userProfilesRepository.findByIdentity !== "function") {
69
69
  throw new Error("authProfileSyncService requires userProfilesRepository.findByIdentity().");
70
70
  }
71
+ if (typeof userProfilesRepository.findByUsername !== "function") {
72
+ throw new Error("authProfileSyncService requires userProfilesRepository.findByUsername().");
73
+ }
71
74
  if (typeof userProfilesRepository.upsert !== "function") {
72
75
  throw new Error("authProfileSyncService requires userProfilesRepository.upsert().");
73
76
  }
@@ -91,6 +94,10 @@ function createService({ userProfilesRepository, lifecycleContributors = [], use
91
94
  );
92
95
  }
93
96
 
97
+ async function findByLogin(login, options = {}) {
98
+ return userProfilesRepository.findByUsername(normalizeLowerText(login), options);
99
+ }
100
+
94
101
  async function upsertByIdentity(profileLike, options = {}) {
95
102
  const normalized = buildNormalizedIdentityProfile(profileLike);
96
103
  return userProfilesRepository.upsert(
@@ -149,6 +156,7 @@ function createService({ userProfilesRepository, lifecycleContributors = [], use
149
156
 
150
157
  return Object.freeze({
151
158
  findByIdentity,
159
+ findByLogin,
152
160
  upsertByIdentity,
153
161
  syncIdentityProfile
154
162
  });
@@ -12,6 +12,9 @@ test("authProfileSyncService.syncIdentityProfile uses shared transaction for pro
12
12
  calls.push({ step: "find", trx: options.trx || null });
13
13
  return null;
14
14
  },
15
+ async findByUsername() {
16
+ return null;
17
+ },
15
18
  async upsert(payload, options = {}) {
16
19
  calls.push({ step: "upsert", trx: options.trx || null });
17
20
  return {
@@ -78,6 +81,9 @@ test("authProfileSyncService.syncIdentityProfile skips write path when profile i
78
81
  displayName: "Tony"
79
82
  };
80
83
  },
84
+ async findByUsername() {
85
+ return null;
86
+ },
81
87
  async upsert() {
82
88
  upsertCalls += 1;
83
89
  return null;
@@ -123,6 +129,9 @@ test("authProfileSyncService.findByIdentity normalizes provider identity input",
123
129
  capturedIdentity = identity;
124
130
  return null;
125
131
  },
132
+ async findByUsername() {
133
+ return null;
134
+ },
126
135
  async upsert() {
127
136
  return null;
128
137
  },
@@ -147,3 +156,42 @@ test("authProfileSyncService.findByIdentity normalizes provider identity input",
147
156
  providerUserId: "user-1"
148
157
  });
149
158
  });
159
+
160
+ test("authProfileSyncService.findByLogin resolves the normalized application username", async () => {
161
+ let capturedUsername = null;
162
+ const expectedProfile = {
163
+ id: "7",
164
+ authProvider: "local",
165
+ authProviderUserSid: "local-user-7",
166
+ email: "ada@example.com",
167
+ username: "merc",
168
+ displayName: "Ada"
169
+ };
170
+ const service = createService({
171
+ userProfilesRepository: {
172
+ async findByIdentity() {
173
+ return null;
174
+ },
175
+ async findByUsername(username) {
176
+ capturedUsername = username;
177
+ return expectedProfile;
178
+ },
179
+ async upsert() {
180
+ return null;
181
+ },
182
+ async withTransaction(work) {
183
+ return work({ trxId: "tx-4" });
184
+ }
185
+ },
186
+ userSettingsRepository: {
187
+ async ensureForUserId() {
188
+ return { userId: "7" };
189
+ }
190
+ }
191
+ });
192
+
193
+ const profile = await service.findByLogin(" MERC ");
194
+
195
+ assert.equal(capturedUsername, "merc");
196
+ assert.equal(profile, expectedProfile);
197
+ });
@@ -328,6 +328,64 @@ test("userProfilesRepository.findByEmail normalizes email lookup", async () => {
328
328
  assert.equal(profile?.displayName, "Ada Example");
329
329
  });
330
330
 
331
+ test("userProfilesRepository.findByUsername normalizes login lookup", async () => {
332
+ const { api, calls } = createUserProfilesApiStub({
333
+ id: 7,
334
+ authProvider: "local",
335
+ authProviderUserSid: "local-user-7",
336
+ email: "ada@example.com",
337
+ username: "merc-user",
338
+ displayName: "Ada Example"
339
+ });
340
+ const repository = createUserProfilesRepository({
341
+ api,
342
+ knex: createKnexStub()
343
+ });
344
+
345
+ const profile = await repository.findByUsername(" Merc User ");
346
+
347
+ assert.deepEqual(calls, [{ username: "merc-user" }]);
348
+ assert.equal(profile?.id, "7");
349
+ assert.equal(profile?.username, "merc-user");
350
+ });
351
+
352
+ test("userProfilesRepository.findByUsername rejects ambiguous login data", async () => {
353
+ const repository = createUserProfilesRepository({
354
+ knex: createKnexStub(),
355
+ api: {
356
+ resources: {
357
+ userProfiles: {
358
+ async query() {
359
+ return asCollectionDocument([
360
+ {
361
+ id: "7",
362
+ authProvider: "local",
363
+ authProviderUserSid: "local-user-7",
364
+ email: "ada@example.com",
365
+ username: "merc",
366
+ displayName: "Ada"
367
+ },
368
+ {
369
+ id: "8",
370
+ authProvider: "local",
371
+ authProviderUserSid: "local-user-8",
372
+ email: "grace@example.com",
373
+ username: "merc",
374
+ displayName: "Grace"
375
+ }
376
+ ]);
377
+ }
378
+ }
379
+ }
380
+ }
381
+ });
382
+
383
+ await assert.rejects(
384
+ () => repository.findByUsername("merc"),
385
+ (error) => error?.code === "USER_PROFILE_LOGIN_AMBIGUOUS"
386
+ );
387
+ });
388
+
331
389
  test("userProfilesRepository.findByIdentity reads existing profiles from collection documents", async () => {
332
390
  const { api, calls } = createUserProfilesApiStub({
333
391
  id: 7,