@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.
- package/package.descriptor.mjs +10 -10
- package/package.json +9 -9
- package/src/server/common/repositories/userProfilesRepository.js +21 -3
- package/src/server/common/services/authProfileSyncService.js +8 -0
- package/test/authProfileSyncService.test.js +48 -0
- package/test/repositoryContracts.test.js +58 -0
package/package.descriptor.mjs
CHANGED
|
@@ -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.
|
|
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.
|
|
150
|
-
"@jskit-ai/crud-core": "0.1.
|
|
151
|
-
"@jskit-ai/database-runtime": "0.1.
|
|
152
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
153
|
-
"@jskit-ai/json-rest-api-core": "0.1.
|
|
154
|
-
"@jskit-ai/kernel": "0.1.
|
|
155
|
-
"@jskit-ai/resource-core": "0.1.
|
|
156
|
-
"@jskit-ai/resource-crud-core": "0.1.
|
|
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.
|
|
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.
|
|
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.
|
|
15
|
-
"@jskit-ai/database-runtime": "0.1.
|
|
16
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
17
|
-
"@jskit-ai/json-rest-api-core": "0.1.
|
|
18
|
-
"@jskit-ai/kernel": "0.1.
|
|
19
|
-
"@jskit-ai/resource-crud-core": "0.1.
|
|
20
|
-
"@jskit-ai/resource-core": "0.1.
|
|
21
|
-
"@jskit-ai/uploads-runtime": "0.1.
|
|
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
|
|
178
|
-
|
|
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
|
-
|
|
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,
|