@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.
- package/package.descriptor.mjs +14 -65
- package/package.json +10 -10
- package/src/server/UsersCoreServiceProvider.js +18 -2
- package/src/server/accountNotifications/accountNotificationsActions.js +3 -5
- package/src/server/accountNotifications/accountNotificationsService.js +3 -2
- package/src/server/accountNotifications/bootAccountNotificationsRoutes.js +12 -11
- package/src/server/accountPreferences/accountPreferencesActions.js +3 -5
- package/src/server/accountPreferences/accountPreferencesService.js +3 -2
- package/src/server/accountPreferences/bootAccountPreferencesRoutes.js +12 -11
- package/src/server/accountProfile/accountProfileActions.js +15 -32
- package/src/server/accountProfile/accountProfileService.js +9 -8
- package/src/server/accountProfile/bootAccountProfileRoutes.js +25 -19
- package/src/server/accountSecurity/accountSecurityActions.js +21 -16
- package/src/server/accountSecurity/accountSecurityService.js +16 -6
- package/src/server/accountSecurity/bootAccountSecurityRoutes.js +52 -40
- package/src/server/common/formatters/accountSettingsResponseFormatter.js +8 -18
- package/src/server/common/registerCommonRepositories.js +5 -2
- package/src/server/common/repositories/userProfilesRepository.js +227 -88
- package/src/server/common/repositories/userSettingsRepository.js +108 -100
- package/src/server/common/support/accountSettingsJsonApiTransport.js +10 -0
- package/src/server/usersBootstrapContributor.js +13 -32
- package/src/shared/resources/accountSettingsSchemas.js +83 -0
- package/src/shared/resources/userProfileResource.js +146 -126
- package/src/shared/resources/userSettingsResource.js +376 -353
- package/templates/packages/users/package.descriptor.mjs +4 -5
- package/templates/packages/users/package.json +0 -1
- package/templates/packages/users/src/server/UsersProvider.js +23 -24
- package/templates/packages/users/src/server/actions.js +26 -28
- package/templates/packages/users/src/server/registerRoutes.js +29 -15
- package/templates/packages/users/src/server/repository.js +35 -28
- package/templates/packages/users/src/server/service.js +20 -15
- package/templates/packages/users/src/shared/userResource.js +55 -68
- package/templates/packages/users-workspace/package.descriptor.mjs +4 -5
- package/templates/packages/users-workspace/src/server/UsersProvider.js +23 -24
- package/templates/packages/users-workspace/src/server/actions.js +28 -28
- package/templates/packages/users-workspace/src/server/registerRoutes.js +34 -16
- package/test/accountSecurityService.test.js +32 -0
- package/test/providerLifecycle.test.js +63 -0
- package/test/registerCommonRepositories.test.js +28 -8
- package/test/repositoryContracts.test.js +177 -28
- package/test/resourcesCanonical.test.js +18 -11
- package/test/userSettingsInternalResource.test.js +8 -0
- package/test/userSettingsResource.test.js +24 -7
- package/test/usersBootstrapContributor.test.js +40 -1
- package/test/usersPackageScaffoldContract.test.js +82 -4
- package/test/usersRouteRequestInputValidator.test.js +92 -23
- package/test/usersRouteResources.test.js +28 -18
- package/src/server/common/resources/userProfilesResource.js +0 -203
- package/src/server/common/validators/authenticatedUserValidator.js +0 -43
- package/src/shared/resources/resolveGlobalArrayRegistry.js +0 -6
- package/src/shared/resources/userSettingsFields.js +0 -76
- package/templates/packages/main/src/shared/resources/userSettingsFields.js +0 -138
- package/templates/packages/users/src/server/actionIds.js +0 -6
- package/templates/packages/users/src/server/listConfig.js +0 -16
- package/test/settingsFieldRegistriesSingleton.test.js +0 -14
- package/test-support/registerDefaultSettingsFields.js +0 -2
|
@@ -1,68 +1,68 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
composeSchemaDefinitions,
|
|
3
|
+
recordIdParamsValidator
|
|
4
|
+
} from "@jskit-ai/kernel/shared/validators";
|
|
2
5
|
import {
|
|
3
6
|
createCrudCursorPaginationQueryValidator,
|
|
4
7
|
listSearchQueryValidator
|
|
5
8
|
} from "@jskit-ai/crud-core/server/listQueryValidators";
|
|
6
9
|
import { workspaceSlugParamsValidator } from "@jskit-ai/workspaces-core/server/validators/routeParamsValidator";
|
|
7
10
|
import { resource } from "../shared/userResource.js";
|
|
8
|
-
import { actionIds } from "./actionIds.js";
|
|
9
|
-
import { LIST_CONFIG } from "./listConfig.js";
|
|
10
11
|
|
|
11
|
-
const listCursorPaginationQueryValidator = createCrudCursorPaginationQueryValidator(
|
|
12
|
+
const listCursorPaginationQueryValidator = createCrudCursorPaginationQueryValidator({
|
|
13
|
+
orderBy: resource.defaultSort
|
|
14
|
+
});
|
|
12
15
|
const authenticatedPermission = Object.freeze({
|
|
13
16
|
require: "authenticated"
|
|
14
17
|
});
|
|
15
18
|
|
|
16
|
-
function
|
|
17
|
-
const normalizedSurface = String(surface || "").trim().toLowerCase();
|
|
18
|
-
if (!normalizedSurface) {
|
|
19
|
-
throw new TypeError("createActions requires a non-empty surface.");
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
return normalizedSurface;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function createActions({ surface = "" } = {}) {
|
|
26
|
-
const actionSurface = requireActionSurface(surface);
|
|
27
|
-
|
|
19
|
+
function createActions({ surface } = {}) {
|
|
28
20
|
return Object.freeze([
|
|
29
21
|
{
|
|
30
|
-
id:
|
|
22
|
+
id: "crud.users.list",
|
|
31
23
|
version: 1,
|
|
32
24
|
kind: "query",
|
|
33
25
|
channels: ["api", "automation", "internal"],
|
|
34
|
-
surfaces: [
|
|
26
|
+
surfaces: [surface],
|
|
35
27
|
permission: authenticatedPermission,
|
|
36
|
-
|
|
37
|
-
|
|
28
|
+
input: composeSchemaDefinitions([
|
|
29
|
+
workspaceSlugParamsValidator,
|
|
30
|
+
listCursorPaginationQueryValidator,
|
|
31
|
+
listSearchQueryValidator
|
|
32
|
+
]),
|
|
33
|
+
output: null,
|
|
38
34
|
idempotency: "none",
|
|
39
35
|
audit: {
|
|
40
|
-
actionName:
|
|
36
|
+
actionName: "crud.users.list"
|
|
41
37
|
},
|
|
42
38
|
observability: {},
|
|
43
39
|
async execute(input, context, deps) {
|
|
44
|
-
|
|
40
|
+
const { workspaceSlug, ...query } = input || {};
|
|
41
|
+
return deps.usersService.queryDocuments(query, {
|
|
45
42
|
context,
|
|
46
43
|
visibilityContext: context?.visibilityContext
|
|
47
44
|
});
|
|
48
45
|
}
|
|
49
46
|
},
|
|
50
47
|
{
|
|
51
|
-
id:
|
|
48
|
+
id: "crud.users.view",
|
|
52
49
|
version: 1,
|
|
53
50
|
kind: "query",
|
|
54
51
|
channels: ["api", "automation", "internal"],
|
|
55
|
-
surfaces: [
|
|
52
|
+
surfaces: [surface],
|
|
56
53
|
permission: authenticatedPermission,
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
input: composeSchemaDefinitions([
|
|
55
|
+
workspaceSlugParamsValidator,
|
|
56
|
+
recordIdParamsValidator
|
|
57
|
+
]),
|
|
58
|
+
output: null,
|
|
59
59
|
idempotency: "none",
|
|
60
60
|
audit: {
|
|
61
|
-
actionName:
|
|
61
|
+
actionName: "crud.users.view"
|
|
62
62
|
},
|
|
63
63
|
observability: {},
|
|
64
64
|
async execute(input, context, deps) {
|
|
65
|
-
return deps.usersService.
|
|
65
|
+
return deps.usersService.getDocumentById(input.recordId, {
|
|
66
66
|
context,
|
|
67
67
|
visibilityContext: context?.visibilityContext
|
|
68
68
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createJsonApiResourceRouteContract } from "@jskit-ai/http-runtime/shared/validators/jsonApiRouteTransport";
|
|
2
2
|
import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
|
|
3
3
|
import {
|
|
4
4
|
createCrudCursorPaginationQueryValidator,
|
|
@@ -6,14 +6,37 @@ import {
|
|
|
6
6
|
} from "@jskit-ai/crud-core/server/listQueryValidators";
|
|
7
7
|
import { resolveScopedApiBasePath } from "@jskit-ai/kernel/shared/surface";
|
|
8
8
|
import { checkRouteVisibility } from "@jskit-ai/kernel/shared/support/visibility";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
composeSchemaDefinitions,
|
|
11
|
+
recordIdParamsValidator
|
|
12
|
+
} from "@jskit-ai/kernel/shared/validators";
|
|
10
13
|
import { routeParamsValidator } from "@jskit-ai/workspaces-core/server/validators/routeParamsValidator";
|
|
11
14
|
import { buildWorkspaceInputFromRouteParams } from "@jskit-ai/workspaces-core/server/support/workspaceRouteInput";
|
|
12
|
-
import { actionIds } from "./actionIds.js";
|
|
13
|
-
import { LIST_CONFIG } from "./listConfig.js";
|
|
14
15
|
import { resource } from "../shared/userResource.js";
|
|
15
16
|
|
|
16
|
-
const listCursorPaginationQueryValidator = createCrudCursorPaginationQueryValidator(
|
|
17
|
+
const listCursorPaginationQueryValidator = createCrudCursorPaginationQueryValidator({
|
|
18
|
+
orderBy: resource.defaultSort
|
|
19
|
+
});
|
|
20
|
+
const listRouteQueryValidator = composeSchemaDefinitions([
|
|
21
|
+
listCursorPaginationQueryValidator,
|
|
22
|
+
listSearchQueryValidator
|
|
23
|
+
]);
|
|
24
|
+
const RESOURCE_ROUTE_CONTRACT_TYPE = resource.namespace;
|
|
25
|
+
const listRouteContract = createJsonApiResourceRouteContract({
|
|
26
|
+
type: RESOURCE_ROUTE_CONTRACT_TYPE,
|
|
27
|
+
query: listRouteQueryValidator,
|
|
28
|
+
output: resource.operations.view.output,
|
|
29
|
+
outputKind: "collection"
|
|
30
|
+
});
|
|
31
|
+
const viewRouteContract = createJsonApiResourceRouteContract({
|
|
32
|
+
type: RESOURCE_ROUTE_CONTRACT_TYPE,
|
|
33
|
+
output: resource.operations.view.output,
|
|
34
|
+
outputKind: "record"
|
|
35
|
+
});
|
|
36
|
+
const viewRouteParamsValidator = composeSchemaDefinitions([
|
|
37
|
+
routeParamsValidator,
|
|
38
|
+
recordIdParamsValidator
|
|
39
|
+
]);
|
|
17
40
|
|
|
18
41
|
function registerRoutes(
|
|
19
42
|
app,
|
|
@@ -43,15 +66,12 @@ function registerRoutes(
|
|
|
43
66
|
tags: ["crud"],
|
|
44
67
|
summary: "List users."
|
|
45
68
|
},
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
responseValidators: withStandardErrorResponses({
|
|
49
|
-
200: resource.operations.list.outputValidator
|
|
50
|
-
})
|
|
69
|
+
...listRouteContract,
|
|
70
|
+
params: routeParamsValidator
|
|
51
71
|
},
|
|
52
72
|
async function (request, reply) {
|
|
53
73
|
const response = await request.executeAction({
|
|
54
|
-
actionId:
|
|
74
|
+
actionId: "crud.users.list",
|
|
55
75
|
input: {
|
|
56
76
|
...buildWorkspaceInputFromRouteParams(request.input.params),
|
|
57
77
|
...(request.input.query || {})
|
|
@@ -72,14 +92,12 @@ function registerRoutes(
|
|
|
72
92
|
tags: ["crud"],
|
|
73
93
|
summary: "View a user."
|
|
74
94
|
},
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
200: resource.operations.view.outputValidator
|
|
78
|
-
})
|
|
95
|
+
...viewRouteContract,
|
|
96
|
+
params: viewRouteParamsValidator
|
|
79
97
|
},
|
|
80
98
|
async function (request, reply) {
|
|
81
99
|
const response = await request.executeAction({
|
|
82
|
-
actionId:
|
|
100
|
+
actionId: "crud.users.view",
|
|
83
101
|
input: {
|
|
84
102
|
...buildWorkspaceInputFromRouteParams(request.input.params),
|
|
85
103
|
recordId: request.input.params.recordId
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { createService } from "../src/server/accountSecurity/accountSecurityService.js";
|
|
4
|
+
|
|
5
|
+
test("account security service returns no content result for other sessions", async () => {
|
|
6
|
+
const calls = [];
|
|
7
|
+
const authService = {
|
|
8
|
+
async signOutOtherSessions(request) {
|
|
9
|
+
calls.push(request);
|
|
10
|
+
return {
|
|
11
|
+
ok: true
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const service = createService({
|
|
17
|
+
userSettingsRepository: {},
|
|
18
|
+
userProfilesRepository: {},
|
|
19
|
+
authService
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const request = {
|
|
23
|
+
id: "request-1"
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const result = await service.logoutOtherSessions(request, {
|
|
27
|
+
id: "user-1"
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
assert.equal(result, null);
|
|
31
|
+
assert.deepEqual(calls, [request]);
|
|
32
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { UsersCoreServiceProvider } from "../src/server/UsersCoreServiceProvider.js";
|
|
4
|
+
import { WorkspacesCoreServiceProvider } from "../../workspaces-core/src/server/WorkspacesCoreServiceProvider.js";
|
|
5
|
+
|
|
6
|
+
function createRegisterPhaseProbe() {
|
|
7
|
+
let makeCalls = 0;
|
|
8
|
+
|
|
9
|
+
const target = {
|
|
10
|
+
singleton() {
|
|
11
|
+
return proxy;
|
|
12
|
+
},
|
|
13
|
+
service() {
|
|
14
|
+
return proxy;
|
|
15
|
+
},
|
|
16
|
+
actions() {
|
|
17
|
+
return proxy;
|
|
18
|
+
},
|
|
19
|
+
instance() {
|
|
20
|
+
return proxy;
|
|
21
|
+
},
|
|
22
|
+
tag() {
|
|
23
|
+
return proxy;
|
|
24
|
+
},
|
|
25
|
+
has() {
|
|
26
|
+
return false;
|
|
27
|
+
},
|
|
28
|
+
make() {
|
|
29
|
+
makeCalls += 1;
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const proxy = new Proxy(target, {
|
|
35
|
+
get(source, property) {
|
|
36
|
+
if (property === "makeCalls") {
|
|
37
|
+
return makeCalls;
|
|
38
|
+
}
|
|
39
|
+
if (Object.prototype.hasOwnProperty.call(source, property)) {
|
|
40
|
+
return source[property];
|
|
41
|
+
}
|
|
42
|
+
return () => proxy;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return proxy;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
test("UsersCoreServiceProvider register phase does not resolve container services eagerly", async () => {
|
|
50
|
+
const app = createRegisterPhaseProbe();
|
|
51
|
+
|
|
52
|
+
await new UsersCoreServiceProvider().register(app);
|
|
53
|
+
|
|
54
|
+
assert.equal(app.makeCalls, 0);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("WorkspacesCoreServiceProvider register phase does not resolve container services eagerly", async () => {
|
|
58
|
+
const app = createRegisterPhaseProbe();
|
|
59
|
+
|
|
60
|
+
await new WorkspacesCoreServiceProvider().register(app);
|
|
61
|
+
|
|
62
|
+
assert.equal(app.makeCalls, 0);
|
|
63
|
+
});
|
|
@@ -18,14 +18,34 @@ test("registerCommonRepositories exposes the shared users-core repositories", as
|
|
|
18
18
|
|
|
19
19
|
const scope = {
|
|
20
20
|
make(token) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
if (token === "internal.json-rest-api") {
|
|
22
|
+
return {
|
|
23
|
+
resources: {
|
|
24
|
+
userSettings: {
|
|
25
|
+
query() {},
|
|
26
|
+
post() {},
|
|
27
|
+
patch() {}
|
|
28
|
+
},
|
|
29
|
+
userProfiles: {
|
|
30
|
+
query() {},
|
|
31
|
+
post() {},
|
|
32
|
+
patch() {}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (token === "jskit.database.knex") {
|
|
39
|
+
return Object.assign(() => {
|
|
40
|
+
throw new Error("query execution not expected");
|
|
41
|
+
}, {
|
|
42
|
+
async transaction(work) {
|
|
43
|
+
return work({ trxId: "trx-1" });
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
throw new Error(`Unexpected token: ${token}`);
|
|
29
49
|
}
|
|
30
50
|
};
|
|
31
51
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
+
import { DEFAULT_USER_SETTINGS } from "../src/shared/settings.js";
|
|
3
4
|
import { createRepository as createUserProfilesRepository } from "../src/server/common/repositories/userProfilesRepository.js";
|
|
4
5
|
import { createRepository as createUserSettingsRepository } from "../src/server/common/repositories/userSettingsRepository.js";
|
|
5
6
|
|
|
@@ -17,7 +18,24 @@ function createKnexStub() {
|
|
|
17
18
|
|
|
18
19
|
test("users-core repositories expose withTransaction", async () => {
|
|
19
20
|
const knex = createKnexStub();
|
|
20
|
-
const
|
|
21
|
+
const api = {
|
|
22
|
+
resources: {
|
|
23
|
+
userProfiles: {
|
|
24
|
+
async query() {
|
|
25
|
+
return { data: [] };
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
userSettings: {
|
|
29
|
+
async query() {
|
|
30
|
+
return { data: [] };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const repositories = [
|
|
36
|
+
createUserProfilesRepository({ api, knex }),
|
|
37
|
+
createUserSettingsRepository({ api, knex })
|
|
38
|
+
];
|
|
21
39
|
|
|
22
40
|
for (const repository of repositories) {
|
|
23
41
|
assert.equal(typeof repository.withTransaction, "function");
|
|
@@ -26,43 +44,171 @@ test("users-core repositories expose withTransaction", async () => {
|
|
|
26
44
|
}
|
|
27
45
|
});
|
|
28
46
|
|
|
29
|
-
function
|
|
47
|
+
function createUserProfilesApiStub(expectedRecord) {
|
|
30
48
|
const calls = [];
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
async
|
|
38
|
-
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
calls,
|
|
52
|
+
api: {
|
|
53
|
+
resources: {
|
|
54
|
+
userProfiles: {
|
|
55
|
+
async query({ queryParams }) {
|
|
56
|
+
calls.push(queryParams?.filters || {});
|
|
57
|
+
return {
|
|
58
|
+
data: expectedRecord ? [{
|
|
59
|
+
type: "userProfiles",
|
|
60
|
+
id: String(expectedRecord.id),
|
|
61
|
+
attributes: {
|
|
62
|
+
authProvider: expectedRecord.authProvider,
|
|
63
|
+
authProviderUserSid: expectedRecord.authProviderUserSid,
|
|
64
|
+
email: expectedRecord.email,
|
|
65
|
+
username: expectedRecord.username,
|
|
66
|
+
displayName: expectedRecord.displayName,
|
|
67
|
+
avatarStorageKey: expectedRecord.avatarStorageKey,
|
|
68
|
+
avatarVersion: expectedRecord.avatarVersion,
|
|
69
|
+
avatarUpdatedAt: expectedRecord.avatarUpdatedAt,
|
|
70
|
+
createdAt: expectedRecord.createdAt
|
|
71
|
+
}
|
|
72
|
+
}] : []
|
|
73
|
+
};
|
|
39
74
|
}
|
|
40
|
-
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
test("userSettingsRepository.ensureForUserId sends transaction outside simplified attributes", async () => {
|
|
82
|
+
const trx = { trxId: "trx-1" };
|
|
83
|
+
let queryCount = 0;
|
|
84
|
+
let postParams = null;
|
|
85
|
+
const repository = createUserSettingsRepository({
|
|
86
|
+
knex: createKnexStub(),
|
|
87
|
+
api: {
|
|
88
|
+
resources: {
|
|
89
|
+
userSettings: {
|
|
90
|
+
async query() {
|
|
91
|
+
queryCount += 1;
|
|
92
|
+
return queryCount < 2
|
|
93
|
+
? { data: [] }
|
|
94
|
+
: {
|
|
95
|
+
data: [{
|
|
96
|
+
type: "userSettings",
|
|
97
|
+
id: "7",
|
|
98
|
+
attributes: {
|
|
99
|
+
...DEFAULT_USER_SETTINGS
|
|
100
|
+
}
|
|
101
|
+
}]
|
|
102
|
+
};
|
|
103
|
+
},
|
|
104
|
+
async post(params) {
|
|
105
|
+
postParams = params;
|
|
106
|
+
return {
|
|
107
|
+
data: {
|
|
108
|
+
type: "userSettings",
|
|
109
|
+
id: "7",
|
|
110
|
+
attributes: {
|
|
111
|
+
...DEFAULT_USER_SETTINGS
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
41
117
|
}
|
|
42
|
-
};
|
|
43
|
-
}, {
|
|
44
|
-
async transaction(work) {
|
|
45
|
-
return work({ trxId: "trx-1" });
|
|
46
118
|
}
|
|
47
119
|
});
|
|
48
120
|
|
|
49
|
-
|
|
50
|
-
|
|
121
|
+
const record = await repository.ensureForUserId("7", { trx });
|
|
122
|
+
|
|
123
|
+
assert.equal(postParams?.simplified, false);
|
|
124
|
+
assert.equal(postParams?.transaction, trx);
|
|
125
|
+
assert.deepEqual(postParams?.inputRecord?.data, {
|
|
126
|
+
type: "userSettings",
|
|
127
|
+
id: "7",
|
|
128
|
+
attributes: {
|
|
129
|
+
id: "7",
|
|
130
|
+
...DEFAULT_USER_SETTINGS
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
assert.equal(record?.id, "7");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("userProfilesRepository.upsert sends native JSON:API write documents with transaction outside the record body", async () => {
|
|
137
|
+
const trx = { trxId: "trx-1" };
|
|
138
|
+
let postParams = null;
|
|
139
|
+
const repository = createUserProfilesRepository({
|
|
140
|
+
knex: createKnexStub(),
|
|
141
|
+
api: {
|
|
142
|
+
resources: {
|
|
143
|
+
userProfiles: {
|
|
144
|
+
async query({ queryParams }) {
|
|
145
|
+
const filters = queryParams?.filters || {};
|
|
146
|
+
if (Object.hasOwn(filters, "authProvider") || Object.hasOwn(filters, "authProviderUserSid")) {
|
|
147
|
+
return { data: [] };
|
|
148
|
+
}
|
|
149
|
+
if (Object.hasOwn(filters, "username")) {
|
|
150
|
+
return { data: [] };
|
|
151
|
+
}
|
|
152
|
+
return { data: [] };
|
|
153
|
+
},
|
|
154
|
+
async post(params) {
|
|
155
|
+
postParams = params;
|
|
156
|
+
const attributes = params.inputRecord?.data?.attributes || {};
|
|
157
|
+
return {
|
|
158
|
+
data: {
|
|
159
|
+
type: "userProfiles",
|
|
160
|
+
id: "11",
|
|
161
|
+
attributes: {
|
|
162
|
+
authProvider: attributes.authProvider,
|
|
163
|
+
authProviderUserSid: attributes.authProviderUserSid,
|
|
164
|
+
email: attributes.email,
|
|
165
|
+
username: attributes.username,
|
|
166
|
+
displayName: attributes.displayName,
|
|
167
|
+
avatarStorageKey: null,
|
|
168
|
+
avatarVersion: null,
|
|
169
|
+
avatarUpdatedAt: null,
|
|
170
|
+
createdAt: attributes.createdAt
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const record = await repository.upsert({
|
|
181
|
+
authProvider: "supabase",
|
|
182
|
+
authProviderUserSid: "user-11",
|
|
183
|
+
email: "ada@example.com",
|
|
184
|
+
displayName: "Ada Example"
|
|
185
|
+
}, { trx });
|
|
186
|
+
|
|
187
|
+
assert.equal(postParams?.simplified, false);
|
|
188
|
+
assert.equal(postParams?.transaction, trx);
|
|
189
|
+
assert.equal(postParams?.inputRecord?.transaction, undefined);
|
|
190
|
+
assert.equal(postParams?.inputRecord?.data?.type, "userProfiles");
|
|
191
|
+
assert.equal(postParams?.inputRecord?.data?.attributes?.authProvider, "supabase");
|
|
192
|
+
assert.equal(record?.id, "11");
|
|
193
|
+
});
|
|
51
194
|
|
|
52
195
|
test("userProfilesRepository.findByEmail normalizes email lookup", async () => {
|
|
53
|
-
const {
|
|
196
|
+
const { api, calls } = createUserProfilesApiStub({
|
|
54
197
|
id: 7,
|
|
55
|
-
|
|
56
|
-
|
|
198
|
+
authProvider: "supabase",
|
|
199
|
+
authProviderUserSid: "supabase-user-7",
|
|
57
200
|
email: "ada@example.com",
|
|
58
201
|
username: "ada",
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
202
|
+
displayName: "Ada Example",
|
|
203
|
+
avatarStorageKey: null,
|
|
204
|
+
avatarVersion: null,
|
|
205
|
+
avatarUpdatedAt: null,
|
|
206
|
+
createdAt: "2026-04-20T00:00:00.000Z"
|
|
207
|
+
});
|
|
208
|
+
const repository = createUserProfilesRepository({
|
|
209
|
+
api,
|
|
210
|
+
knex: createKnexStub()
|
|
64
211
|
});
|
|
65
|
-
const repository = createUserProfilesRepository(knex);
|
|
66
212
|
|
|
67
213
|
const profile = await repository.findByEmail(" ADA@EXAMPLE.COM ");
|
|
68
214
|
|
|
@@ -73,8 +219,11 @@ test("userProfilesRepository.findByEmail normalizes email lookup", async () => {
|
|
|
73
219
|
});
|
|
74
220
|
|
|
75
221
|
test("userProfilesRepository.findByEmail returns null when the row is missing", async () => {
|
|
76
|
-
const {
|
|
77
|
-
const repository = createUserProfilesRepository(
|
|
222
|
+
const { api } = createUserProfilesApiStub(undefined);
|
|
223
|
+
const repository = createUserProfilesRepository({
|
|
224
|
+
api,
|
|
225
|
+
knex: createKnexStub()
|
|
226
|
+
});
|
|
78
227
|
|
|
79
228
|
const profile = await repository.findByEmail("missing@example.com");
|
|
80
229
|
|
|
@@ -3,7 +3,7 @@ import assert from "node:assert/strict";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { existsSync } from "node:fs";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import "
|
|
6
|
+
import { resolveStructuredSchemaTransportSchema } from "@jskit-ai/kernel/shared/validators";
|
|
7
7
|
import { userProfileResource } from "../src/shared/resources/userProfileResource.js";
|
|
8
8
|
import { userSettingsResource } from "../src/shared/resources/userSettingsResource.js";
|
|
9
9
|
|
|
@@ -51,22 +51,29 @@ test("users-core specialized resource operations expose messages and validators"
|
|
|
51
51
|
|
|
52
52
|
for (const { label, operation } of operationSpecs) {
|
|
53
53
|
assert.equal(typeof operation?.messages, "object", `${label}.messages must be an object.`);
|
|
54
|
-
assert.equal(
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
assert.equal(
|
|
55
|
+
typeof resolveStructuredSchemaTransportSchema(operation?.output, {
|
|
56
|
+
context: `${label}.output`,
|
|
57
|
+
defaultMode: "replace"
|
|
58
|
+
}),
|
|
59
|
+
"object",
|
|
60
|
+
`${label}.output transport schema must exist.`
|
|
61
|
+
);
|
|
62
|
+
if (operation?.body) {
|
|
63
|
+
assert.equal(typeof operation.body.schema, "object", `${label}.body.schema must exist.`);
|
|
57
64
|
}
|
|
58
|
-
if (operation?.
|
|
59
|
-
assert.equal(typeof operation.
|
|
65
|
+
if (operation?.params) {
|
|
66
|
+
assert.equal(typeof operation.params.schema, "object", `${label}.params.schema must exist.`);
|
|
60
67
|
}
|
|
61
|
-
if (operation?.
|
|
62
|
-
assert.equal(typeof operation.
|
|
68
|
+
if (operation?.query) {
|
|
69
|
+
assert.equal(typeof operation.query.schema, "object", `${label}.query.schema must exist.`);
|
|
63
70
|
}
|
|
64
71
|
}
|
|
65
72
|
});
|
|
66
73
|
|
|
67
|
-
test("users-core
|
|
74
|
+
test("users-core does not contain src/shared/schema", () => {
|
|
68
75
|
const testFilePath = fileURLToPath(import.meta.url);
|
|
69
76
|
const packageRoot = path.resolve(path.dirname(testFilePath), "..");
|
|
70
|
-
const
|
|
71
|
-
assert.equal(existsSync(
|
|
77
|
+
const sharedSchemaDirPath = path.join(packageRoot, "src", "shared", "schema");
|
|
78
|
+
assert.equal(existsSync(sharedSchemaDirPath), false, "src/shared/schema must not exist.");
|
|
72
79
|
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { userSettingsResource } from "../src/shared/resources/userSettingsResource.js";
|
|
4
|
+
|
|
5
|
+
test("shared user settings resource declares user_id as the resource id column", () => {
|
|
6
|
+
assert.equal(userSettingsResource.idProperty, "user_id");
|
|
7
|
+
assert.equal(userSettingsResource.schema.id?.storage?.column, "user_id");
|
|
8
|
+
});
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
import { validateOperationSection } from "@jskit-ai/http-runtime/shared/validators/operationValidation";
|
|
4
|
-
import
|
|
5
|
-
|
|
4
|
+
import {
|
|
5
|
+
USER_SETTINGS_ALL_KEYS,
|
|
6
|
+
userSettingsResource
|
|
7
|
+
} from "../src/shared/resources/userSettingsResource.js";
|
|
6
8
|
|
|
7
9
|
function parseBody(operation, payload = {}) {
|
|
8
10
|
return validateOperationSection({
|
|
9
11
|
operation,
|
|
10
|
-
section: "
|
|
12
|
+
section: "body",
|
|
11
13
|
value: payload
|
|
12
14
|
});
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
test("user settings preferences update keeps required string validation
|
|
16
|
-
const parsed = parseBody(userSettingsResource.operations.preferencesUpdate, {
|
|
17
|
+
test("user settings preferences update keeps required string validation", async () => {
|
|
18
|
+
const parsed = await parseBody(userSettingsResource.operations.preferencesUpdate, {
|
|
17
19
|
theme: " "
|
|
18
20
|
});
|
|
19
21
|
|
|
@@ -21,11 +23,26 @@ test("user settings preferences update keeps required string validation after no
|
|
|
21
23
|
assert.equal(typeof parsed.fieldErrors.theme, "string");
|
|
22
24
|
});
|
|
23
25
|
|
|
24
|
-
test("user settings notifications update rejects non-boolean values", () => {
|
|
25
|
-
const parsed = parseBody(userSettingsResource.operations.notificationsUpdate, {
|
|
26
|
+
test("user settings notifications update rejects non-boolean values", async () => {
|
|
27
|
+
const parsed = await parseBody(userSettingsResource.operations.notificationsUpdate, {
|
|
26
28
|
productUpdates: "yes"
|
|
27
29
|
});
|
|
28
30
|
|
|
29
31
|
assert.equal(parsed.ok, false);
|
|
30
32
|
assert.equal(typeof parsed.fieldErrors.productUpdates, "string");
|
|
31
33
|
});
|
|
34
|
+
|
|
35
|
+
async function importWithIdentity(url, identity) {
|
|
36
|
+
return import(`${url.href}?identity=${identity}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
test("user settings key exports stay stable across module identities", async () => {
|
|
40
|
+
const userModuleUrl = new URL("../src/shared/resources/userSettingsResource.js", import.meta.url);
|
|
41
|
+
|
|
42
|
+
const userA = await importWithIdentity(userModuleUrl, "user-a");
|
|
43
|
+
const userB = await importWithIdentity(userModuleUrl, "user-b");
|
|
44
|
+
|
|
45
|
+
assert.deepEqual(userA.USER_SETTINGS_ALL_KEYS, userB.USER_SETTINGS_ALL_KEYS);
|
|
46
|
+
assert.deepEqual(userA.USER_SETTINGS_ALL_KEYS, USER_SETTINGS_ALL_KEYS);
|
|
47
|
+
assert.ok(Object.isFrozen(userA.USER_SETTINGS_ALL_KEYS));
|
|
48
|
+
});
|