@jskit-ai/users-core 0.1.55 → 0.1.57
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 +175 -6
- package/package.json +6 -6
- package/src/server/accountNotifications/accountNotificationsService.js +3 -3
- package/src/server/accountNotifications/registerAccountNotifications.js +2 -2
- package/src/server/accountPreferences/accountPreferencesService.js +3 -3
- package/src/server/accountPreferences/registerAccountPreferences.js +2 -2
- package/src/server/accountProfile/accountProfileService.js +5 -5
- package/src/server/accountProfile/avatarService.js +6 -6
- package/src/server/accountProfile/registerAccountProfile.js +3 -3
- package/src/server/accountSecurity/accountSecurityService.js +3 -3
- package/src/server/accountSecurity/registerAccountSecurity.js +2 -2
- package/src/server/common/registerCommonRepositories.js +5 -5
- package/src/server/common/repositories/{usersRepository.js → userProfilesRepository.js} +117 -95
- package/src/server/common/repositories/userSettingsRepository.js +12 -11
- package/src/server/common/resources/userProfilesResource.js +203 -0
- package/src/server/common/services/accountContextService.js +4 -4
- package/src/server/common/services/authProfileSyncService.js +11 -11
- package/src/server/common/support/identity.js +17 -0
- package/src/server/registerUsersBootstrap.js +2 -2
- package/src/server/registerUsersCore.js +2 -2
- package/src/server/usersBootstrapContributor.js +5 -5
- package/src/shared/resources/userProfileResource.js +1 -1
- package/src/shared/resources/userSettingsFields.js +10 -4
- package/src/shared/resources/userSettingsResource.js +1 -1
- package/templates/packages/main/src/shared/resources/userSettingsFields.js +10 -10
- package/templates/packages/users/package.descriptor.mjs +65 -0
- package/templates/packages/users/package.json +10 -0
- package/templates/packages/users/src/server/UsersProvider.js +91 -0
- package/templates/packages/users/src/server/actionIds.js +6 -0
- package/templates/packages/users/src/server/actions.js +73 -0
- package/templates/packages/users/src/server/listConfig.js +16 -0
- package/templates/packages/users/src/server/registerRoutes.js +87 -0
- package/templates/packages/users/src/server/repository.js +41 -0
- package/templates/packages/users/src/server/service.js +28 -0
- package/templates/packages/users/src/shared/index.js +1 -0
- package/templates/packages/users/src/shared/userResource.js +74 -0
- package/templates/packages/users-workspace/package.descriptor.mjs +66 -0
- package/templates/packages/users-workspace/src/server/UsersProvider.js +92 -0
- package/templates/packages/users-workspace/src/server/actions.js +74 -0
- package/templates/packages/users-workspace/src/server/registerRoutes.js +93 -0
- package/test/authProfileSyncService.test.js +3 -3
- package/test/avatarService.test.js +2 -2
- package/test/registerCommonRepositories.test.js +37 -0
- package/test/repositoryContracts.test.js +48 -5
- package/test/usersBootstrapContributor.test.js +2 -2
- package/test/usersPackageScaffoldContract.test.js +98 -0
- package/test/usersRouteResources.test.js +1 -1
|
@@ -1,37 +1,29 @@
|
|
|
1
|
+
import { createCrudResourceRuntime } from "@jskit-ai/crud-core/server/resourceRuntime";
|
|
1
2
|
import {
|
|
2
3
|
isDuplicateEntryError,
|
|
3
|
-
normalizeLowerText,
|
|
4
4
|
normalizeDbRecordId,
|
|
5
|
+
normalizeLowerText,
|
|
5
6
|
normalizeRecordId,
|
|
6
|
-
|
|
7
|
-
toIsoString,
|
|
8
|
-
toNullableDateTime,
|
|
9
|
-
toNullableIso,
|
|
10
|
-
nowDb,
|
|
11
|
-
createWithTransaction
|
|
7
|
+
nowDb
|
|
12
8
|
} from "./repositoryUtils.js";
|
|
9
|
+
import { normalizeIdentity } from "../support/identity.js";
|
|
10
|
+
import { resource } from "../resources/userProfilesResource.js";
|
|
13
11
|
|
|
14
12
|
const USERNAME_MAX_LENGTH = 120;
|
|
13
|
+
const REPOSITORY_CONFIG = Object.freeze({
|
|
14
|
+
context: "internal.repository.user-profiles"
|
|
15
|
+
});
|
|
15
16
|
|
|
16
|
-
function
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
return {
|
|
24
|
-
provider,
|
|
25
|
-
providerUserId
|
|
26
|
-
};
|
|
17
|
+
function normalizeProfileRecord(payload = {}) {
|
|
18
|
+
return resource.operations.view.outputValidator.normalize(payload);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeCreatePayload(payload = {}) {
|
|
22
|
+
return resource.operations.create.bodyValidator.normalize(payload);
|
|
27
23
|
}
|
|
28
24
|
|
|
29
25
|
function normalizeUsername(value) {
|
|
30
|
-
|
|
31
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
32
|
-
.replace(/^-+|-+$/g, "")
|
|
33
|
-
.slice(0, USERNAME_MAX_LENGTH);
|
|
34
|
-
return normalized || "";
|
|
26
|
+
return normalizeCreatePayload({ username: value }).username || "";
|
|
35
27
|
}
|
|
36
28
|
|
|
37
29
|
function usernameBaseFromEmail(email) {
|
|
@@ -53,28 +45,11 @@ function buildUsernameCandidate(baseUsername, suffix) {
|
|
|
53
45
|
return `${trimmedBase}${suffixText}`;
|
|
54
46
|
}
|
|
55
47
|
|
|
56
|
-
function mapProfileRow(row) {
|
|
57
|
-
if (!row) {
|
|
58
|
-
return null;
|
|
59
|
-
}
|
|
60
|
-
return {
|
|
61
|
-
id: normalizeDbRecordId(row.id, { fallback: "" }),
|
|
62
|
-
authProvider: normalizeLowerText(row.auth_provider),
|
|
63
|
-
authProviderUserSid: normalizeText(row.auth_provider_user_sid),
|
|
64
|
-
email: normalizeLowerText(row.email),
|
|
65
|
-
username: normalizeLowerText(row.username),
|
|
66
|
-
displayName: normalizeText(row.display_name),
|
|
67
|
-
avatarStorageKey: row.avatar_storage_key ? normalizeText(row.avatar_storage_key) : null,
|
|
68
|
-
avatarVersion: row.avatar_version == null ? null : String(row.avatar_version),
|
|
69
|
-
avatarUpdatedAt: toNullableIso(row.avatar_updated_at),
|
|
70
|
-
createdAt: toIsoString(row.created_at)
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
48
|
function duplicateTargetsEmail(error) {
|
|
75
49
|
if (!isDuplicateEntryError(error)) {
|
|
76
50
|
return false;
|
|
77
51
|
}
|
|
52
|
+
|
|
78
53
|
const message = normalizeLowerText(error?.sqlMessage || error?.message);
|
|
79
54
|
return message.includes("email");
|
|
80
55
|
}
|
|
@@ -83,6 +58,7 @@ function duplicateTargetsUsername(error) {
|
|
|
83
58
|
if (!isDuplicateEntryError(error)) {
|
|
84
59
|
return false;
|
|
85
60
|
}
|
|
61
|
+
|
|
86
62
|
const message = normalizeLowerText(error?.sqlMessage || error?.message);
|
|
87
63
|
return message.includes("username");
|
|
88
64
|
}
|
|
@@ -109,9 +85,11 @@ async function resolveUniqueUsername(client, baseUsername, { excludeUserId = nul
|
|
|
109
85
|
|
|
110
86
|
function createRepository(knex) {
|
|
111
87
|
if (typeof knex !== "function") {
|
|
112
|
-
throw new TypeError("
|
|
88
|
+
throw new TypeError("internal.repository.user-profiles requires knex.");
|
|
113
89
|
}
|
|
114
|
-
|
|
90
|
+
|
|
91
|
+
const resourceRuntime = createCrudResourceRuntime(resource, knex, REPOSITORY_CONFIG);
|
|
92
|
+
const withTransaction = resourceRuntime.withTransaction;
|
|
115
93
|
|
|
116
94
|
async function findById(userId, options = {}) {
|
|
117
95
|
const normalizedUserId = normalizeRecordId(userId, { fallback: null });
|
|
@@ -119,25 +97,38 @@ function createRepository(knex) {
|
|
|
119
97
|
return null;
|
|
120
98
|
}
|
|
121
99
|
|
|
100
|
+
return resourceRuntime.findById(normalizedUserId, {
|
|
101
|
+
...options,
|
|
102
|
+
include: "none"
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function findByEmail(email, options = {}) {
|
|
107
|
+
const normalizedEmail = normalizeCreatePayload({ email }).email || "";
|
|
108
|
+
if (!normalizedEmail) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
122
112
|
const client = options?.trx || knex;
|
|
123
|
-
const row = await client("users").where({
|
|
124
|
-
return
|
|
113
|
+
const row = await client("users").where({ email: normalizedEmail }).first();
|
|
114
|
+
return row ? normalizeProfileRecord(row) : null;
|
|
125
115
|
}
|
|
126
116
|
|
|
127
117
|
async function findByIdentity(identityLike, options = {}) {
|
|
128
|
-
const client = options?.trx || knex;
|
|
129
118
|
const identity = normalizeIdentity(identityLike);
|
|
130
119
|
if (!identity) {
|
|
131
120
|
return null;
|
|
132
121
|
}
|
|
133
122
|
|
|
123
|
+
const client = options?.trx || knex;
|
|
134
124
|
const row = await client("users")
|
|
135
125
|
.where({
|
|
136
126
|
auth_provider: identity.provider,
|
|
137
127
|
auth_provider_user_sid: identity.providerUserId
|
|
138
128
|
})
|
|
139
129
|
.first();
|
|
140
|
-
|
|
130
|
+
|
|
131
|
+
return row ? normalizeProfileRecord(row) : null;
|
|
141
132
|
}
|
|
142
133
|
|
|
143
134
|
async function updateDisplayNameById(userId, displayName, options = {}) {
|
|
@@ -146,13 +137,14 @@ function createRepository(knex) {
|
|
|
146
137
|
return null;
|
|
147
138
|
}
|
|
148
139
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
140
|
+
return resourceRuntime.updateById(
|
|
141
|
+
normalizedUserId,
|
|
142
|
+
{ displayName },
|
|
143
|
+
{
|
|
144
|
+
...options,
|
|
145
|
+
include: "none"
|
|
146
|
+
}
|
|
147
|
+
);
|
|
156
148
|
}
|
|
157
149
|
|
|
158
150
|
async function updateAvatarById(userId, avatar = {}, options = {}) {
|
|
@@ -161,16 +153,18 @@ function createRepository(knex) {
|
|
|
161
153
|
return null;
|
|
162
154
|
}
|
|
163
155
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
156
|
+
return resourceRuntime.updateById(
|
|
157
|
+
normalizedUserId,
|
|
158
|
+
{
|
|
159
|
+
avatarStorageKey: avatar.avatarStorageKey ?? null,
|
|
160
|
+
avatarVersion: avatar.avatarVersion ?? null,
|
|
161
|
+
avatarUpdatedAt: avatar.avatarUpdatedAt ?? nowDb()
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
...options,
|
|
165
|
+
include: "none"
|
|
166
|
+
}
|
|
167
|
+
);
|
|
174
168
|
}
|
|
175
169
|
|
|
176
170
|
async function clearAvatarById(userId, options = {}) {
|
|
@@ -179,26 +173,33 @@ function createRepository(knex) {
|
|
|
179
173
|
return null;
|
|
180
174
|
}
|
|
181
175
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
176
|
+
return resourceRuntime.updateById(
|
|
177
|
+
normalizedUserId,
|
|
178
|
+
{
|
|
179
|
+
avatarStorageKey: null,
|
|
180
|
+
avatarVersion: null,
|
|
181
|
+
avatarUpdatedAt: null
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
...options,
|
|
185
|
+
include: "none"
|
|
186
|
+
}
|
|
187
|
+
);
|
|
191
188
|
}
|
|
192
189
|
|
|
193
190
|
async function upsert(profileLike = {}, options = {}) {
|
|
194
|
-
const
|
|
191
|
+
const normalizedPayload = normalizeCreatePayload(profileLike);
|
|
192
|
+
const identity = normalizeIdentity({
|
|
193
|
+
provider: normalizedPayload.authProvider,
|
|
194
|
+
providerUserId: normalizedPayload.authProviderUserSid
|
|
195
|
+
});
|
|
195
196
|
if (!identity) {
|
|
196
197
|
throw new TypeError("upsert requires provider/authProvider and providerUserId/authProviderUserSid.");
|
|
197
198
|
}
|
|
198
199
|
|
|
199
|
-
const email =
|
|
200
|
-
const displayName =
|
|
201
|
-
const requestedUsername = normalizeUsername(
|
|
200
|
+
const email = normalizedPayload.email || "";
|
|
201
|
+
const displayName = normalizedPayload.displayName || "";
|
|
202
|
+
const requestedUsername = normalizeUsername(normalizedPayload.username);
|
|
202
203
|
if (!email || !displayName) {
|
|
203
204
|
throw new TypeError("upsert requires email and displayName.");
|
|
204
205
|
}
|
|
@@ -213,24 +214,44 @@ function createRepository(knex) {
|
|
|
213
214
|
try {
|
|
214
215
|
if (existing) {
|
|
215
216
|
const existingUsername = normalizeUsername(existing.username);
|
|
216
|
-
const username = existingUsername || (
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
217
|
+
const username = existingUsername || (
|
|
218
|
+
await resolveUniqueUsername(
|
|
219
|
+
trx,
|
|
220
|
+
requestedUsername || usernameBaseFromEmail(email),
|
|
221
|
+
{ excludeUserId: existing.id }
|
|
222
|
+
)
|
|
223
|
+
);
|
|
224
|
+
return resourceRuntime.updateById(
|
|
225
|
+
normalizeDbRecordId(existing.id, { fallback: null }),
|
|
226
|
+
{
|
|
227
|
+
email,
|
|
228
|
+
displayName,
|
|
229
|
+
username
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
trx,
|
|
233
|
+
include: "none"
|
|
234
|
+
}
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const username = await resolveUniqueUsername(
|
|
239
|
+
trx,
|
|
240
|
+
requestedUsername || usernameBaseFromEmail(email)
|
|
241
|
+
);
|
|
242
|
+
return resourceRuntime.create(
|
|
243
|
+
{
|
|
244
|
+
authProvider: identity.provider,
|
|
245
|
+
authProviderUserSid: identity.providerUserId,
|
|
229
246
|
email,
|
|
230
|
-
|
|
247
|
+
displayName,
|
|
231
248
|
username
|
|
232
|
-
}
|
|
233
|
-
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
trx,
|
|
252
|
+
include: "none"
|
|
253
|
+
}
|
|
254
|
+
);
|
|
234
255
|
} catch (error) {
|
|
235
256
|
if (duplicateTargetsEmail(error)) {
|
|
236
257
|
throw createDuplicateEmailConflictError();
|
|
@@ -244,7 +265,7 @@ function createRepository(knex) {
|
|
|
244
265
|
}
|
|
245
266
|
|
|
246
267
|
const resolved = await trx("users").where(where).first();
|
|
247
|
-
return
|
|
268
|
+
return resolved ? normalizeProfileRecord(resolved) : null;
|
|
248
269
|
};
|
|
249
270
|
|
|
250
271
|
if (options?.trx) {
|
|
@@ -257,6 +278,7 @@ function createRepository(knex) {
|
|
|
257
278
|
return Object.freeze({
|
|
258
279
|
withTransaction,
|
|
259
280
|
findById,
|
|
281
|
+
findByEmail,
|
|
260
282
|
findByIdentity,
|
|
261
283
|
updateDisplayNameById,
|
|
262
284
|
updateAvatarById,
|
|
@@ -265,4 +287,4 @@ function createRepository(knex) {
|
|
|
265
287
|
});
|
|
266
288
|
}
|
|
267
289
|
|
|
268
|
-
export { createRepository
|
|
290
|
+
export { createRepository };
|
|
@@ -25,8 +25,9 @@ function mapRow(row) {
|
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
for (const field of userSettingsFields) {
|
|
28
|
-
const
|
|
29
|
-
|
|
28
|
+
const column = field.repository.column;
|
|
29
|
+
const value = Object.hasOwn(row, column)
|
|
30
|
+
? row[column]
|
|
30
31
|
: field.resolveDefault({
|
|
31
32
|
settings: mapped,
|
|
32
33
|
row
|
|
@@ -66,7 +67,7 @@ function createInsertPayload(userId) {
|
|
|
66
67
|
const defaultValue = field.resolveDefault({
|
|
67
68
|
settings: resolvedDefaults
|
|
68
69
|
});
|
|
69
|
-
payload[field.
|
|
70
|
+
payload[field.repository.column] = field.normalizeInput(defaultValue, {
|
|
70
71
|
payload: resolvedDefaults,
|
|
71
72
|
settings: resolvedDefaults
|
|
72
73
|
});
|
|
@@ -132,14 +133,14 @@ function createRepository(knex) {
|
|
|
132
133
|
updated_at: nowDb()
|
|
133
134
|
};
|
|
134
135
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
136
|
+
for (const field of userSettingsFields) {
|
|
137
|
+
if (!Object.hasOwn(source, field.key)) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
dbPatch[field.repository.column] = field.normalizeInput(source[field.key], {
|
|
141
|
+
payload: source,
|
|
142
|
+
settings: ensured
|
|
143
|
+
});
|
|
143
144
|
}
|
|
144
145
|
|
|
145
146
|
if (Object.hasOwn(source, "passwordSignInEnabled")) {
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { normalizeDbRecordId, toIsoString, toNullableDateTime } from "@jskit-ai/database-runtime/shared";
|
|
3
|
+
import {
|
|
4
|
+
createCursorListValidator,
|
|
5
|
+
normalizeObjectInput,
|
|
6
|
+
recordIdSchema
|
|
7
|
+
} from "@jskit-ai/kernel/shared/validators";
|
|
8
|
+
import {
|
|
9
|
+
normalizeIfPresent,
|
|
10
|
+
normalizeLowerText,
|
|
11
|
+
normalizeText,
|
|
12
|
+
normalizeOrNull
|
|
13
|
+
} from "@jskit-ai/kernel/shared/support/normalize";
|
|
14
|
+
|
|
15
|
+
const USERNAME_MAX_LENGTH = 120;
|
|
16
|
+
|
|
17
|
+
function normalizeUsername(value) {
|
|
18
|
+
const normalized = normalizeLowerText(value)
|
|
19
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
20
|
+
.replace(/^-+|-+$/g, "")
|
|
21
|
+
.slice(0, USERNAME_MAX_LENGTH);
|
|
22
|
+
|
|
23
|
+
return normalized || "";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalizeNullableString(value) {
|
|
27
|
+
if (value === null || value === undefined) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return normalizeText(value);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizeNullableVersion(value) {
|
|
35
|
+
if (value === null || value === undefined || value === "") {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return String(value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizeProfileRecord(payload = {}) {
|
|
43
|
+
const source = normalizeObjectInput(payload);
|
|
44
|
+
const id = normalizeIfPresent(source.id, (value) => normalizeDbRecordId(value, { fallback: null }));
|
|
45
|
+
const displayName = normalizeText(source.displayName ?? source.display_name);
|
|
46
|
+
const email = normalizeLowerText(source.email);
|
|
47
|
+
const username = normalizeUsername(source.username);
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
id,
|
|
51
|
+
authProvider: normalizeLowerText(source.authProvider ?? source.auth_provider),
|
|
52
|
+
authProviderUserSid: normalizeText(source.authProviderUserSid ?? source.auth_provider_user_sid),
|
|
53
|
+
email,
|
|
54
|
+
username,
|
|
55
|
+
displayName,
|
|
56
|
+
avatarStorageKey: normalizeOrNull(source.avatarStorageKey ?? source.avatar_storage_key, normalizeNullableString),
|
|
57
|
+
avatarVersion: normalizeNullableVersion(source.avatarVersion ?? source.avatar_version),
|
|
58
|
+
avatarUpdatedAt: normalizeOrNull(source.avatarUpdatedAt ?? source.avatar_updated_at, toIsoString),
|
|
59
|
+
createdAt: normalizeIfPresent(source.createdAt ?? source.created_at, toIsoString)
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function normalizeCreatePayload(payload = {}) {
|
|
64
|
+
const source = normalizeObjectInput(payload);
|
|
65
|
+
const normalized = {};
|
|
66
|
+
|
|
67
|
+
if (Object.hasOwn(source, "authProvider") || Object.hasOwn(source, "provider")) {
|
|
68
|
+
normalized.authProvider = normalizeLowerText(source.authProvider ?? source.provider);
|
|
69
|
+
}
|
|
70
|
+
if (Object.hasOwn(source, "authProviderUserSid") || Object.hasOwn(source, "providerUserId")) {
|
|
71
|
+
normalized.authProviderUserSid = normalizeText(source.authProviderUserSid ?? source.providerUserId);
|
|
72
|
+
}
|
|
73
|
+
if (Object.hasOwn(source, "email")) {
|
|
74
|
+
normalized.email = normalizeLowerText(source.email);
|
|
75
|
+
}
|
|
76
|
+
if (Object.hasOwn(source, "username")) {
|
|
77
|
+
normalized.username = normalizeUsername(source.username);
|
|
78
|
+
}
|
|
79
|
+
if (Object.hasOwn(source, "displayName")) {
|
|
80
|
+
normalized.displayName = normalizeText(source.displayName);
|
|
81
|
+
}
|
|
82
|
+
if (Object.hasOwn(source, "avatarStorageKey")) {
|
|
83
|
+
normalized.avatarStorageKey = normalizeNullableString(source.avatarStorageKey);
|
|
84
|
+
}
|
|
85
|
+
if (Object.hasOwn(source, "avatarVersion")) {
|
|
86
|
+
normalized.avatarVersion = normalizeNullableVersion(source.avatarVersion);
|
|
87
|
+
}
|
|
88
|
+
if (Object.hasOwn(source, "avatarUpdatedAt")) {
|
|
89
|
+
normalized.avatarUpdatedAt = toNullableDateTime(source.avatarUpdatedAt);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return normalized;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const recordOutputSchema = Type.Object(
|
|
96
|
+
{
|
|
97
|
+
id: recordIdSchema,
|
|
98
|
+
authProvider: Type.String({ minLength: 1 }),
|
|
99
|
+
authProviderUserSid: Type.String({ minLength: 1 }),
|
|
100
|
+
email: Type.String({ minLength: 1 }),
|
|
101
|
+
username: Type.String({ minLength: 1 }),
|
|
102
|
+
displayName: Type.String({ minLength: 1 }),
|
|
103
|
+
avatarStorageKey: Type.Union([Type.String(), Type.Null()]),
|
|
104
|
+
avatarVersion: Type.Union([Type.String(), Type.Null()]),
|
|
105
|
+
avatarUpdatedAt: Type.Union([Type.String({ format: "date-time", minLength: 1 }), Type.Null()]),
|
|
106
|
+
createdAt: Type.String({ format: "date-time", minLength: 1 })
|
|
107
|
+
},
|
|
108
|
+
{ additionalProperties: false }
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const createBodySchema = Type.Object(
|
|
112
|
+
{
|
|
113
|
+
authProvider: Type.String({ minLength: 1, maxLength: 64 }),
|
|
114
|
+
authProviderUserSid: Type.String({ minLength: 1, maxLength: 191 }),
|
|
115
|
+
email: Type.String({ minLength: 1, maxLength: 255 }),
|
|
116
|
+
username: Type.Optional(Type.String({ minLength: 1, maxLength: USERNAME_MAX_LENGTH })),
|
|
117
|
+
displayName: Type.String({ minLength: 1, maxLength: 160 }),
|
|
118
|
+
avatarStorageKey: Type.Optional(Type.Union([Type.String({ maxLength: 512 }), Type.Null()])),
|
|
119
|
+
avatarVersion: Type.Optional(Type.Union([Type.String({ maxLength: 64 }), Type.Null()])),
|
|
120
|
+
avatarUpdatedAt: Type.Optional(Type.Union([Type.String({ format: "date-time", minLength: 1 }), Type.Null()]))
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
additionalProperties: false,
|
|
124
|
+
required: []
|
|
125
|
+
}
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const patchBodySchema = Type.Partial(createBodySchema, {
|
|
129
|
+
additionalProperties: false
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const recordOutputValidator = Object.freeze({
|
|
133
|
+
schema: recordOutputSchema,
|
|
134
|
+
normalize: normalizeProfileRecord
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const createBodyValidator = Object.freeze({
|
|
138
|
+
schema: createBodySchema,
|
|
139
|
+
normalize: normalizeCreatePayload
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const patchBodyValidator = Object.freeze({
|
|
143
|
+
schema: patchBodySchema,
|
|
144
|
+
normalize: normalizeCreatePayload
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const resource = Object.freeze({
|
|
148
|
+
namespace: "userProfiles",
|
|
149
|
+
tableName: "users",
|
|
150
|
+
idColumn: "id",
|
|
151
|
+
operations: Object.freeze({
|
|
152
|
+
list: Object.freeze({
|
|
153
|
+
method: "GET",
|
|
154
|
+
outputValidator: createCursorListValidator(recordOutputValidator)
|
|
155
|
+
}),
|
|
156
|
+
view: Object.freeze({
|
|
157
|
+
method: "GET",
|
|
158
|
+
outputValidator: recordOutputValidator
|
|
159
|
+
}),
|
|
160
|
+
create: Object.freeze({
|
|
161
|
+
method: "POST",
|
|
162
|
+
bodyValidator: createBodyValidator,
|
|
163
|
+
outputValidator: recordOutputValidator
|
|
164
|
+
}),
|
|
165
|
+
patch: Object.freeze({
|
|
166
|
+
method: "PATCH",
|
|
167
|
+
bodyValidator: patchBodyValidator,
|
|
168
|
+
outputValidator: recordOutputValidator
|
|
169
|
+
})
|
|
170
|
+
}),
|
|
171
|
+
fieldMeta: Object.freeze([
|
|
172
|
+
Object.freeze({
|
|
173
|
+
key: "authProvider",
|
|
174
|
+
repository: { column: "auth_provider" }
|
|
175
|
+
}),
|
|
176
|
+
Object.freeze({
|
|
177
|
+
key: "authProviderUserSid",
|
|
178
|
+
repository: { column: "auth_provider_user_sid" }
|
|
179
|
+
}),
|
|
180
|
+
Object.freeze({
|
|
181
|
+
key: "displayName",
|
|
182
|
+
repository: { column: "display_name" }
|
|
183
|
+
}),
|
|
184
|
+
Object.freeze({
|
|
185
|
+
key: "avatarStorageKey",
|
|
186
|
+
repository: { column: "avatar_storage_key" }
|
|
187
|
+
}),
|
|
188
|
+
Object.freeze({
|
|
189
|
+
key: "avatarVersion",
|
|
190
|
+
repository: { column: "avatar_version" }
|
|
191
|
+
}),
|
|
192
|
+
Object.freeze({
|
|
193
|
+
key: "avatarUpdatedAt",
|
|
194
|
+
repository: { column: "avatar_updated_at" }
|
|
195
|
+
}),
|
|
196
|
+
Object.freeze({
|
|
197
|
+
key: "createdAt",
|
|
198
|
+
repository: { column: "created_at" }
|
|
199
|
+
})
|
|
200
|
+
])
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
export { resource };
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
-
import { normalizeIdentity } from "../
|
|
2
|
+
import { normalizeIdentity } from "../support/identity.js";
|
|
3
3
|
|
|
4
|
-
async function resolveUserProfile(
|
|
4
|
+
async function resolveUserProfile(userProfilesRepository, user) {
|
|
5
5
|
const identity = normalizeIdentity(user);
|
|
6
6
|
if (identity) {
|
|
7
|
-
const profile = await
|
|
7
|
+
const profile = await userProfilesRepository.findByIdentity(identity);
|
|
8
8
|
if (profile) {
|
|
9
9
|
return profile;
|
|
10
10
|
}
|
|
@@ -12,7 +12,7 @@ async function resolveUserProfile(usersRepository, user) {
|
|
|
12
12
|
|
|
13
13
|
const userId = normalizeRecordId(user?.id, { fallback: null });
|
|
14
14
|
if (userId) {
|
|
15
|
-
const profileById = await
|
|
15
|
+
const profileById = await userProfilesRepository.findById(userId);
|
|
16
16
|
if (profileById) {
|
|
17
17
|
return profileById;
|
|
18
18
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
2
|
import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/actions/textNormalization";
|
|
3
|
-
import { normalizeIdentity } from "../
|
|
3
|
+
import { normalizeIdentity } from "../support/identity.js";
|
|
4
4
|
|
|
5
5
|
function buildNormalizedIdentityKey(identityLike) {
|
|
6
6
|
const identity = normalizeIdentity(identityLike);
|
|
@@ -64,15 +64,15 @@ function normalizeLifecycleContributors(entries = []) {
|
|
|
64
64
|
);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
function createService({
|
|
68
|
-
if (!
|
|
69
|
-
throw new Error("authProfileSyncService requires
|
|
67
|
+
function createService({ userProfilesRepository, lifecycleContributors = [], userSettingsRepository = null } = {}) {
|
|
68
|
+
if (!userProfilesRepository || typeof userProfilesRepository.findByIdentity !== "function") {
|
|
69
|
+
throw new Error("authProfileSyncService requires userProfilesRepository.findByIdentity().");
|
|
70
70
|
}
|
|
71
|
-
if (typeof
|
|
72
|
-
throw new Error("authProfileSyncService requires
|
|
71
|
+
if (typeof userProfilesRepository.upsert !== "function") {
|
|
72
|
+
throw new Error("authProfileSyncService requires userProfilesRepository.upsert().");
|
|
73
73
|
}
|
|
74
|
-
if (typeof
|
|
75
|
-
throw new Error("authProfileSyncService requires
|
|
74
|
+
if (typeof userProfilesRepository.withTransaction !== "function") {
|
|
75
|
+
throw new Error("authProfileSyncService requires userProfilesRepository.withTransaction().");
|
|
76
76
|
}
|
|
77
77
|
if (!userSettingsRepository || typeof userSettingsRepository.ensureForUserId !== "function") {
|
|
78
78
|
throw new Error("authProfileSyncService requires userSettingsRepository.ensureForUserId().");
|
|
@@ -82,7 +82,7 @@ function createService({ usersRepository, lifecycleContributors = [], userSettin
|
|
|
82
82
|
|
|
83
83
|
async function findByIdentity(identityLike, options = {}) {
|
|
84
84
|
const normalized = buildNormalizedIdentityKey(identityLike);
|
|
85
|
-
return
|
|
85
|
+
return userProfilesRepository.findByIdentity(
|
|
86
86
|
{
|
|
87
87
|
provider: normalized.authProvider,
|
|
88
88
|
providerUserId: normalized.authProviderUserSid
|
|
@@ -93,7 +93,7 @@ function createService({ usersRepository, lifecycleContributors = [], userSettin
|
|
|
93
93
|
|
|
94
94
|
async function upsertByIdentity(profileLike, options = {}) {
|
|
95
95
|
const normalized = buildNormalizedIdentityProfile(profileLike);
|
|
96
|
-
return
|
|
96
|
+
return userProfilesRepository.upsert(
|
|
97
97
|
{
|
|
98
98
|
authProvider: normalized.authProvider,
|
|
99
99
|
authProviderUserSid: normalized.authProviderUserSid,
|
|
@@ -144,7 +144,7 @@ function createService({ usersRepository, lifecycleContributors = [], userSettin
|
|
|
144
144
|
return runSync(options.trx);
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
return
|
|
147
|
+
return userProfilesRepository.withTransaction((trx) => runSync(trx));
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
return Object.freeze({
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
+
|
|
3
|
+
function normalizeIdentity(identityLike) {
|
|
4
|
+
const source = identityLike && typeof identityLike === "object" ? identityLike : {};
|
|
5
|
+
const provider = normalizeLowerText(source.provider || source.authProvider);
|
|
6
|
+
const providerUserId = normalizeText(source.providerUserId || source.authProviderUserSid);
|
|
7
|
+
if (!provider || !providerUserId) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
provider,
|
|
13
|
+
providerUserId
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export { normalizeIdentity };
|
|
@@ -9,8 +9,8 @@ function registerUsersBootstrap(app) {
|
|
|
9
9
|
|
|
10
10
|
registerBootstrapPayloadContributor(app, "users.core.bootstrap.payloadContributor", (scope) => {
|
|
11
11
|
return createUsersBootstrapContributor({
|
|
12
|
-
|
|
13
|
-
userSettingsRepository: scope.make("
|
|
12
|
+
userProfilesRepository: scope.make("internal.repository.user-profiles"),
|
|
13
|
+
userSettingsRepository: scope.make("internal.repository.user-settings"),
|
|
14
14
|
appConfig: resolveAppConfig(scope),
|
|
15
15
|
authService: scope.make("authService")
|
|
16
16
|
});
|