@jskit-ai/users-core 0.1.64 → 0.1.66
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 +70 -3
- 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
|
@@ -97,7 +97,7 @@ test("users bootstrap contributor exposes the generic authenticated bootstrap pa
|
|
|
97
97
|
}
|
|
98
98
|
]);
|
|
99
99
|
assert.equal(payload.session.oauthDefaultProvider, "google");
|
|
100
|
-
assert.deepEqual(payload.userSettings,
|
|
100
|
+
assert.deepEqual(payload.userSettings, createUserSettings());
|
|
101
101
|
assert.equal(payload.requestMeta.hasRequest, true);
|
|
102
102
|
});
|
|
103
103
|
|
|
@@ -152,3 +152,42 @@ test("users bootstrap contributor emits anonymous bootstrap payload without work
|
|
|
152
152
|
});
|
|
153
153
|
assert.equal(payload.userSettings, null);
|
|
154
154
|
});
|
|
155
|
+
|
|
156
|
+
test("users bootstrap contributor uses shared boolean normalization for app feature flags", async () => {
|
|
157
|
+
const contributor = createUsersBootstrapContributor({
|
|
158
|
+
userProfilesRepository: {
|
|
159
|
+
async findById() {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
userSettingsRepository: {
|
|
164
|
+
async ensureForUserId() {
|
|
165
|
+
return createUserSettings();
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
appConfig: {
|
|
169
|
+
assistantEnabled: "yes",
|
|
170
|
+
socialEnabled: 0,
|
|
171
|
+
socialFederationEnabled: "no"
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const payload = await contributor.contribute({
|
|
176
|
+
request: {
|
|
177
|
+
async executeAction() {
|
|
178
|
+
return {
|
|
179
|
+
authenticated: false
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
payload: {},
|
|
184
|
+
reply: {}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
assert.deepEqual(payload.app.features, {
|
|
188
|
+
assistantEnabled: true,
|
|
189
|
+
assistantRequiredPermission: "",
|
|
190
|
+
socialEnabled: false,
|
|
191
|
+
socialFederationEnabled: false
|
|
192
|
+
});
|
|
193
|
+
});
|
|
@@ -4,6 +4,8 @@ import path from "node:path";
|
|
|
4
4
|
import test from "node:test";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import descriptor from "../package.descriptor.mjs";
|
|
7
|
+
import crudCorePackage from "../../crud-core/package.json" with { type: "json" };
|
|
8
|
+
import resourceCrudCorePackage from "../../resource-crud-core/package.json" with { type: "json" };
|
|
7
9
|
|
|
8
10
|
const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
|
|
9
11
|
const PACKAGE_ROOT = path.resolve(TEST_DIRECTORY, "..");
|
|
@@ -14,7 +16,11 @@ function readFileMutationById(id) {
|
|
|
14
16
|
|
|
15
17
|
test("users-core installs the app-local users package scaffold", () => {
|
|
16
18
|
assert.equal(descriptor.mutations.dependencies.runtime["@local/users"], "file:packages/users");
|
|
17
|
-
assert.equal(descriptor.mutations.dependencies.runtime["@jskit-ai/crud-core"],
|
|
19
|
+
assert.equal(descriptor.mutations.dependencies.runtime["@jskit-ai/crud-core"], crudCorePackage.version);
|
|
20
|
+
assert.equal(
|
|
21
|
+
descriptor.mutations.dependencies.runtime["@jskit-ai/resource-crud-core"],
|
|
22
|
+
resourceCrudCorePackage.version
|
|
23
|
+
);
|
|
18
24
|
|
|
19
25
|
const expectedFileIds = [
|
|
20
26
|
"users-core-users-package-json",
|
|
@@ -22,10 +28,8 @@ test("users-core installs the app-local users package scaffold", () => {
|
|
|
22
28
|
"users-core-users-package-descriptor-workspace",
|
|
23
29
|
"users-core-users-provider-base",
|
|
24
30
|
"users-core-users-provider-workspace",
|
|
25
|
-
"users-core-users-action-ids",
|
|
26
31
|
"users-core-users-actions-base",
|
|
27
32
|
"users-core-users-actions-workspace",
|
|
28
|
-
"users-core-users-list-config",
|
|
29
33
|
"users-core-users-routes-base",
|
|
30
34
|
"users-core-users-routes-workspace",
|
|
31
35
|
"users-core-users-repository",
|
|
@@ -58,17 +62,55 @@ test("users-core base users package templates stay aligned with non-workspace ap
|
|
|
58
62
|
path.join(PACKAGE_ROOT, "templates/packages/users/src/server/actions.js"),
|
|
59
63
|
"utf8"
|
|
60
64
|
);
|
|
65
|
+
const repositorySource = await readFile(
|
|
66
|
+
path.join(PACKAGE_ROOT, "templates/packages/users/src/server/repository.js"),
|
|
67
|
+
"utf8"
|
|
68
|
+
);
|
|
69
|
+
const serviceSource = await readFile(
|
|
70
|
+
path.join(PACKAGE_ROOT, "templates/packages/users/src/server/service.js"),
|
|
71
|
+
"utf8"
|
|
72
|
+
);
|
|
61
73
|
const routesSource = await readFile(
|
|
62
74
|
path.join(PACKAGE_ROOT, "templates/packages/users/src/server/registerRoutes.js"),
|
|
63
75
|
"utf8"
|
|
64
76
|
);
|
|
65
77
|
|
|
66
78
|
assert.doesNotMatch(packageDescriptorSource, /@jskit-ai\/workspaces-core/);
|
|
79
|
+
assert.match(packageDescriptorSource, /@jskit-ai\/json-rest-api-core/);
|
|
80
|
+
assert.match(packageDescriptorSource, /json-rest-api\.core/);
|
|
81
|
+
assert.doesNotMatch(packageDescriptorSource, /server\/actionIds/);
|
|
67
82
|
assert.match(providerSource, /surface: "home"/);
|
|
68
83
|
assert.doesNotMatch(providerSource, /routeSurfaceRequiresWorkspace/);
|
|
84
|
+
assert.doesNotMatch(providerSource, /createCrudLookup/);
|
|
85
|
+
assert.doesNotMatch(providerSource, /lookup\.users/);
|
|
86
|
+
assert.doesNotMatch(providerSource, /normalizeRecordId/);
|
|
87
|
+
assert.doesNotMatch(providerSource, /requires application singleton\(\)\/service\(\)\/actions\(\)\./);
|
|
88
|
+
assert.match(providerSource, /createJsonRestResourceScopeOptions/);
|
|
89
|
+
assert.match(providerSource, /addResourceIfMissing\(\s*api,\s*"users",\s*createJsonRestResourceScopeOptions\(resource,/s);
|
|
90
|
+
assert.match(repositorySource, /api\.resources\.users\.query\(/);
|
|
91
|
+
assert.match(repositorySource, /api\.resources\.users\.get\(/);
|
|
92
|
+
assert.match(repositorySource, /async function queryDocuments\(query = \{\}, options = \{\}\)/);
|
|
93
|
+
assert.match(repositorySource, /async function getDocumentById\(recordId, options = \{\}\)/);
|
|
94
|
+
assert.match(repositorySource, /returnNullWhenJsonRestResourceMissing/);
|
|
69
95
|
assert.doesNotMatch(actionsSource, /workspaceSlugParamsValidator/);
|
|
96
|
+
assert.doesNotMatch(actionsSource, /requireActionSurface/);
|
|
97
|
+
assert.match(actionsSource, /orderBy: resource\.defaultSort/);
|
|
98
|
+
assert.match(actionsSource, /output: null/);
|
|
99
|
+
assert.match(actionsSource, /usersService\.queryDocuments/);
|
|
100
|
+
assert.match(actionsSource, /usersService\.getDocumentById/);
|
|
101
|
+
assert.doesNotMatch(actionsSource, /from "\.\/actionIds\.js"/);
|
|
102
|
+
assert.match(actionsSource, /id: "crud\.users\.list"/);
|
|
103
|
+
assert.match(actionsSource, /id: "crud\.users\.view"/);
|
|
104
|
+
assert.doesNotMatch(serviceSource, /serviceEvents/);
|
|
105
|
+
assert.match(serviceSource, /throw new TypeError\("createService requires usersRepository\."\);/);
|
|
106
|
+
assert.match(serviceSource, /return404IfNotFound/);
|
|
107
|
+
assert.match(serviceSource, /throw new AppError\(404, "Document not found\."\);/);
|
|
108
|
+
assert.match(serviceSource, /returnJsonApiDocument/);
|
|
70
109
|
assert.doesNotMatch(routesSource, /workspaceRouteInput/);
|
|
110
|
+
assert.match(routesSource, /createJsonApiResourceRouteContract/);
|
|
111
|
+
assert.doesNotMatch(routesSource, /wrapResponse/);
|
|
71
112
|
assert.match(routesSource, /routeBase: "\/"/);
|
|
113
|
+
assert.match(routesSource, /orderBy: resource\.defaultSort/);
|
|
72
114
|
});
|
|
73
115
|
|
|
74
116
|
test("users-core workspace users package templates stay aligned with workspace apps", async () => {
|
|
@@ -88,11 +130,36 @@ test("users-core workspace users package templates stay aligned with workspace a
|
|
|
88
130
|
path.join(PACKAGE_ROOT, "templates/packages/users-workspace/src/server/registerRoutes.js"),
|
|
89
131
|
"utf8"
|
|
90
132
|
);
|
|
133
|
+
const serviceSource = await readFile(
|
|
134
|
+
path.join(PACKAGE_ROOT, "templates/packages/users/src/server/service.js"),
|
|
135
|
+
"utf8"
|
|
136
|
+
);
|
|
91
137
|
|
|
92
138
|
assert.match(packageDescriptorSource, /@jskit-ai\/workspaces-core/);
|
|
139
|
+
assert.match(packageDescriptorSource, /@jskit-ai\/json-rest-api-core/);
|
|
140
|
+
assert.match(packageDescriptorSource, /json-rest-api\.core/);
|
|
141
|
+
assert.doesNotMatch(packageDescriptorSource, /server\/actionIds/);
|
|
93
142
|
assert.match(providerSource, /surface: "admin"/);
|
|
94
143
|
assert.match(providerSource, /routeSurfaceRequiresWorkspace/);
|
|
144
|
+
assert.doesNotMatch(providerSource, /createCrudLookup/);
|
|
145
|
+
assert.doesNotMatch(providerSource, /lookup\.users/);
|
|
146
|
+
assert.doesNotMatch(providerSource, /normalizeRecordId/);
|
|
147
|
+
assert.doesNotMatch(providerSource, /requires application singleton\(\)\/service\(\)\/actions\(\)\./);
|
|
148
|
+
assert.match(providerSource, /createJsonRestResourceScopeOptions/);
|
|
149
|
+
assert.match(providerSource, /addResourceIfMissing\(\s*api,\s*"users",\s*createJsonRestResourceScopeOptions\(resource,/s);
|
|
95
150
|
assert.match(actionsSource, /workspaceSlugParamsValidator/);
|
|
151
|
+
assert.doesNotMatch(actionsSource, /requireActionSurface/);
|
|
152
|
+
assert.match(actionsSource, /orderBy: resource\.defaultSort/);
|
|
153
|
+
assert.match(actionsSource, /usersService\.queryDocuments/);
|
|
154
|
+
assert.match(actionsSource, /usersService\.getDocumentById/);
|
|
155
|
+
assert.doesNotMatch(actionsSource, /from "\.\/actionIds\.js"/);
|
|
156
|
+
assert.match(actionsSource, /id: "crud\.users\.list"/);
|
|
157
|
+
assert.match(actionsSource, /id: "crud\.users\.view"/);
|
|
96
158
|
assert.match(routesSource, /buildWorkspaceInputFromRouteParams/);
|
|
159
|
+
assert.match(routesSource, /createJsonApiResourceRouteContract/);
|
|
160
|
+
assert.doesNotMatch(routesSource, /wrapResponse/);
|
|
97
161
|
assert.match(routesSource, /routeBase: routeSurfaceRequiresWorkspace === true \? "\/w\/:workspaceSlug" : "\/"/);
|
|
162
|
+
assert.doesNotMatch(serviceSource, /serviceEvents/);
|
|
163
|
+
assert.match(serviceSource, /throw new TypeError\("createService requires usersRepository\."\);/);
|
|
164
|
+
assert.match(serviceSource, /returnJsonApiDocument/);
|
|
98
165
|
});
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import { UsersCoreServiceProvider } from "../src/server/UsersCoreServiceProvider.js";
|
|
4
|
+
import { INTERNAL_JSON_REST_API } from "@jskit-ai/json-rest-api-core/server/jsonRestApiHost";
|
|
5
|
+
import { createRouter } from "../../kernel/server/http/lib/router.js";
|
|
4
6
|
|
|
5
7
|
function createReplyDouble() {
|
|
6
8
|
return {
|
|
@@ -29,21 +31,18 @@ function findRoute(routes, { method, path }) {
|
|
|
29
31
|
async function registerRoutes({
|
|
30
32
|
authService = {}
|
|
31
33
|
} = {}) {
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
method,
|
|
38
|
-
path,
|
|
39
|
-
handler
|
|
40
|
-
});
|
|
34
|
+
const router = createRouter();
|
|
35
|
+
const internalApi = {
|
|
36
|
+
resources: {},
|
|
37
|
+
async addResource(scopeName) {
|
|
38
|
+
this.resources[scopeName] = {};
|
|
41
39
|
}
|
|
42
40
|
};
|
|
43
41
|
|
|
44
42
|
const bindings = new Map([
|
|
45
43
|
["jskit.http.router", router],
|
|
46
44
|
["authService", authService],
|
|
45
|
+
[INTERNAL_JSON_REST_API, internalApi],
|
|
47
46
|
[
|
|
48
47
|
"users.accountProfile.service",
|
|
49
48
|
{
|
|
@@ -62,6 +61,10 @@ async function registerRoutes({
|
|
|
62
61
|
has(token) {
|
|
63
62
|
return bindings.has(token);
|
|
64
63
|
},
|
|
64
|
+
instance(token, value) {
|
|
65
|
+
bindings.set(token, value);
|
|
66
|
+
return this;
|
|
67
|
+
},
|
|
65
68
|
make(token) {
|
|
66
69
|
if (!bindings.has(token)) {
|
|
67
70
|
throw new Error(`Missing test binding for token: ${String(token)}`);
|
|
@@ -73,7 +76,7 @@ async function registerRoutes({
|
|
|
73
76
|
const provider = new UsersCoreServiceProvider();
|
|
74
77
|
await provider.boot(app);
|
|
75
78
|
|
|
76
|
-
return
|
|
79
|
+
return router.list();
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
function createActionRequest({ input = {}, executeAction, file = null }) {
|
|
@@ -89,9 +92,34 @@ function createActionRequest({ input = {}, executeAction, file = null }) {
|
|
|
89
92
|
|
|
90
93
|
test("users-core boot mounts account routes without workspace routes", async () => {
|
|
91
94
|
const routes = await registerRoutes();
|
|
95
|
+
const settingsRoute = findRoute(routes, { method: "GET", path: "/api/settings" });
|
|
96
|
+
const profileRoute = findRoute(routes, { method: "PATCH", path: "/api/settings/profile" });
|
|
97
|
+
const preferencesRoute = findRoute(routes, { method: "PATCH", path: "/api/settings/preferences" });
|
|
98
|
+
const notificationsRoute = findRoute(routes, { method: "PATCH", path: "/api/settings/notifications" });
|
|
92
99
|
|
|
93
|
-
assert.equal(
|
|
94
|
-
assert.equal(
|
|
100
|
+
assert.equal(settingsRoute?.path, "/api/settings");
|
|
101
|
+
assert.equal(profileRoute?.path, "/api/settings/profile");
|
|
102
|
+
assert.equal(preferencesRoute?.path, "/api/settings/preferences");
|
|
103
|
+
assert.equal(notificationsRoute?.path, "/api/settings/notifications");
|
|
104
|
+
assert.equal(settingsRoute?.transport?.kind, "jsonapi-resource");
|
|
105
|
+
assert.equal(profileRoute?.transport?.kind, "jsonapi-resource");
|
|
106
|
+
assert.equal(preferencesRoute?.transport?.kind, "jsonapi-resource");
|
|
107
|
+
assert.equal(notificationsRoute?.transport?.kind, "jsonapi-resource");
|
|
108
|
+
assert.equal(settingsRoute?.schema?.response?.[200]?.required?.[0], "data");
|
|
109
|
+
assert.equal(profileRoute?.schema?.body?.required?.[0], "data");
|
|
110
|
+
assert.equal(profileRoute?.schema?.response?.[200]?.required?.[0], "data");
|
|
111
|
+
assert.equal(
|
|
112
|
+
profileRoute?.schema?.body?.definitions?.["user-profilesRequestResource"]?.properties?.type?.const,
|
|
113
|
+
"user-profiles"
|
|
114
|
+
);
|
|
115
|
+
assert.equal(
|
|
116
|
+
settingsRoute?.schema?.response?.[200]?.definitions?.["user-settingsSuccessResource"]?.properties?.type?.const,
|
|
117
|
+
"user-settings"
|
|
118
|
+
);
|
|
119
|
+
assert.equal(
|
|
120
|
+
typeof findRoute(routes, { method: "GET", path: "/api/settings/security/oauth/:provider/start" })?.schema?.response?.[302],
|
|
121
|
+
"object"
|
|
122
|
+
);
|
|
95
123
|
assert.equal(findRoute(routes, { method: "GET", path: "/api/workspaces" }), null);
|
|
96
124
|
assert.equal(findRoute(routes, { method: "GET", path: "/api/w/:workspaceSlug/settings" }), null);
|
|
97
125
|
});
|
|
@@ -109,10 +137,10 @@ test("account route handlers build action input from request.input", async () =>
|
|
|
109
137
|
return { url: "/oauth/link" };
|
|
110
138
|
}
|
|
111
139
|
if (payload.actionId === "settings.profile.update") {
|
|
112
|
-
return {
|
|
140
|
+
return { response: { __jskitJsonApiResult: true, kind: "data", value: {} }, session: null };
|
|
113
141
|
}
|
|
114
142
|
if (payload.actionId === "settings.security.password.change") {
|
|
115
|
-
return { message: "ok", session: null };
|
|
143
|
+
return { response: { __jskitJsonApiResult: true, kind: "meta", value: { message: "ok" } }, session: null };
|
|
116
144
|
}
|
|
117
145
|
return {};
|
|
118
146
|
};
|
|
@@ -183,19 +211,60 @@ test("account route handlers build action input from request.input", async () =>
|
|
|
183
211
|
createReplyDouble()
|
|
184
212
|
);
|
|
185
213
|
|
|
186
|
-
assert.deepEqual(calls[0].input, {
|
|
187
|
-
assert.deepEqual(calls[1].input, {
|
|
188
|
-
assert.deepEqual(calls[2].input, {
|
|
214
|
+
assert.deepEqual(calls[0].input, { displayName: "Merc" });
|
|
215
|
+
assert.deepEqual(calls[1].input, { locale: "en-US" });
|
|
216
|
+
assert.deepEqual(calls[2].input, { email: true });
|
|
189
217
|
assert.deepEqual(calls[3].input, {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
confirmPassword: "new-password-123"
|
|
194
|
-
}
|
|
218
|
+
currentPassword: "old-password",
|
|
219
|
+
newPassword: "new-password-123",
|
|
220
|
+
confirmPassword: "new-password-123"
|
|
195
221
|
});
|
|
196
|
-
assert.deepEqual(calls[4].input, {
|
|
222
|
+
assert.deepEqual(calls[4].input, { enabled: true });
|
|
197
223
|
assert.deepEqual(calls[5].input, { provider: "github", returnTo: "/app/settings" });
|
|
198
224
|
assert.equal(oauthReply.redirectedTo, "/oauth/link");
|
|
199
225
|
assert.deepEqual(calls[6].input, { provider: "github" });
|
|
200
226
|
assert.equal(calls[7].actionId, "settings.security.sessions.logout_others");
|
|
201
227
|
});
|
|
228
|
+
|
|
229
|
+
test("account settings jsonapi transport resolves response resource id from request user", async () => {
|
|
230
|
+
const routes = await registerRoutes();
|
|
231
|
+
const settingsRoute = findRoute(routes, { method: "GET", path: "/api/settings" });
|
|
232
|
+
|
|
233
|
+
const document = settingsRoute.transport.response(
|
|
234
|
+
{
|
|
235
|
+
__jskitJsonApiResult: true,
|
|
236
|
+
kind: "data",
|
|
237
|
+
value: {
|
|
238
|
+
profile: {
|
|
239
|
+
displayName: "Merc"
|
|
240
|
+
},
|
|
241
|
+
security: {},
|
|
242
|
+
preferences: {},
|
|
243
|
+
notifications: {}
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
request: {
|
|
248
|
+
user: {
|
|
249
|
+
id: 42
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
statusCode: 200
|
|
253
|
+
}
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
assert.deepEqual(document, {
|
|
257
|
+
data: {
|
|
258
|
+
type: "user-settings",
|
|
259
|
+
id: "42",
|
|
260
|
+
attributes: {
|
|
261
|
+
profile: {
|
|
262
|
+
displayName: "Merc"
|
|
263
|
+
},
|
|
264
|
+
security: {},
|
|
265
|
+
preferences: {},
|
|
266
|
+
notifications: {}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
});
|
|
@@ -4,7 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { existsSync } from "node:fs";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { deriveResourceRequiredMetadata } from "@jskit-ai/kernel/_testable";
|
|
7
|
-
import "
|
|
7
|
+
import { resolveStructuredSchemaTransportSchema } from "@jskit-ai/kernel/shared/validators";
|
|
8
8
|
import { userProfileResource } from "../src/shared/resources/userProfileResource.js";
|
|
9
9
|
import { userSettingsResource } from "../src/shared/resources/userSettingsResource.js";
|
|
10
10
|
|
|
@@ -27,15 +27,18 @@ function assertResourceShape(resource, label) {
|
|
|
27
27
|
`${label}.operations.${operationName} must resolve messages from operation.messages or resource.messages.`
|
|
28
28
|
);
|
|
29
29
|
assert.equal(
|
|
30
|
-
typeof operation.
|
|
30
|
+
typeof resolveStructuredSchemaTransportSchema(operation.output, {
|
|
31
|
+
context: `${label}.operations.${operationName}.output`,
|
|
32
|
+
defaultMode: "replace"
|
|
33
|
+
}),
|
|
31
34
|
"object",
|
|
32
35
|
`${label}.operations.${operationName} payload schema is required.`
|
|
33
36
|
);
|
|
34
37
|
}
|
|
35
38
|
|
|
36
|
-
assert.equal(typeof resource.operations.create.
|
|
37
|
-
assert.equal(typeof resource.operations.replace.
|
|
38
|
-
assert.equal(typeof resource.operations.patch.
|
|
39
|
+
assert.equal(typeof resource.operations.create.body?.schema, "object", `${label}.operations.create.body.schema is required.`);
|
|
40
|
+
assert.equal(typeof resource.operations.replace.body?.schema, "object", `${label}.operations.replace.body.schema is required.`);
|
|
41
|
+
assert.equal(typeof resource.operations.patch.body?.schema, "object", `${label}.operations.patch.body.schema is required.`);
|
|
39
42
|
|
|
40
43
|
const requiredMetadata = deriveResourceRequiredMetadata(resource);
|
|
41
44
|
assert.ok(Array.isArray(requiredMetadata.create), `${label}.derivedRequired.create must be an array.`);
|
|
@@ -67,29 +70,36 @@ test("specialized settings and invite operations expose canonical validators", (
|
|
|
67
70
|
|
|
68
71
|
for (const { label, operation } of operationSpecs) {
|
|
69
72
|
assert.equal(typeof operation?.method, "string", `${label}.method must exist.`);
|
|
70
|
-
assert.equal(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
assert.equal(
|
|
74
|
+
typeof resolveStructuredSchemaTransportSchema(operation?.output, {
|
|
75
|
+
context: `${label}.output`,
|
|
76
|
+
defaultMode: "replace"
|
|
77
|
+
}),
|
|
78
|
+
"object",
|
|
79
|
+
`${label}.output transport schema must exist.`
|
|
80
|
+
);
|
|
81
|
+
if (operation?.body) {
|
|
82
|
+
assert.equal(typeof operation.body.schema, "object", `${label}.body.schema must exist.`);
|
|
73
83
|
}
|
|
74
|
-
if (operation?.
|
|
75
|
-
assert.equal(typeof operation.
|
|
84
|
+
if (operation?.params) {
|
|
85
|
+
assert.equal(typeof operation.params.schema, "object", `${label}.params.schema must exist.`);
|
|
76
86
|
}
|
|
77
|
-
if (operation?.
|
|
78
|
-
assert.equal(typeof operation.
|
|
87
|
+
if (operation?.query) {
|
|
88
|
+
assert.equal(typeof operation.query.schema, "object", `${label}.query.schema must exist.`);
|
|
79
89
|
}
|
|
80
90
|
}
|
|
81
91
|
});
|
|
82
92
|
|
|
83
|
-
test("users-core
|
|
93
|
+
test("users-core does not use workspaceRoutes.js helper that exposes raw schema leaves", () => {
|
|
84
94
|
const testFilePath = fileURLToPath(import.meta.url);
|
|
85
95
|
const packageRoot = path.resolve(path.dirname(testFilePath), "..");
|
|
86
|
-
const
|
|
87
|
-
assert.equal(existsSync(
|
|
96
|
+
const workspaceRoutesFilePath = path.join(packageRoot, "src", "server", "common", "routes", "workspaceRoutes.js");
|
|
97
|
+
assert.equal(existsSync(workspaceRoutesFilePath), false, "workspaceRoutes.js must not exist.");
|
|
88
98
|
});
|
|
89
99
|
|
|
90
|
-
test("users-core route validators
|
|
100
|
+
test("users-core route validators do not live under src/shared/schema", () => {
|
|
91
101
|
const testFilePath = fileURLToPath(import.meta.url);
|
|
92
102
|
const packageRoot = path.resolve(path.dirname(testFilePath), "..");
|
|
93
|
-
const
|
|
94
|
-
assert.equal(existsSync(
|
|
103
|
+
const sharedSchemaDirPath = path.join(packageRoot, "src", "shared", "schema");
|
|
104
|
+
assert.equal(existsSync(sharedSchemaDirPath), false, "src/shared/schema must not exist.");
|
|
95
105
|
});
|
|
@@ -1,203 +0,0 @@
|
|
|
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 };
|