@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.
Files changed (56) hide show
  1. package/package.descriptor.mjs +14 -65
  2. package/package.json +10 -10
  3. package/src/server/UsersCoreServiceProvider.js +18 -2
  4. package/src/server/accountNotifications/accountNotificationsActions.js +3 -5
  5. package/src/server/accountNotifications/accountNotificationsService.js +3 -2
  6. package/src/server/accountNotifications/bootAccountNotificationsRoutes.js +12 -11
  7. package/src/server/accountPreferences/accountPreferencesActions.js +3 -5
  8. package/src/server/accountPreferences/accountPreferencesService.js +3 -2
  9. package/src/server/accountPreferences/bootAccountPreferencesRoutes.js +12 -11
  10. package/src/server/accountProfile/accountProfileActions.js +15 -32
  11. package/src/server/accountProfile/accountProfileService.js +9 -8
  12. package/src/server/accountProfile/bootAccountProfileRoutes.js +25 -19
  13. package/src/server/accountSecurity/accountSecurityActions.js +21 -16
  14. package/src/server/accountSecurity/accountSecurityService.js +16 -6
  15. package/src/server/accountSecurity/bootAccountSecurityRoutes.js +52 -40
  16. package/src/server/common/formatters/accountSettingsResponseFormatter.js +8 -18
  17. package/src/server/common/registerCommonRepositories.js +5 -2
  18. package/src/server/common/repositories/userProfilesRepository.js +227 -88
  19. package/src/server/common/repositories/userSettingsRepository.js +108 -100
  20. package/src/server/common/support/accountSettingsJsonApiTransport.js +10 -0
  21. package/src/server/usersBootstrapContributor.js +13 -32
  22. package/src/shared/resources/accountSettingsSchemas.js +83 -0
  23. package/src/shared/resources/userProfileResource.js +146 -126
  24. package/src/shared/resources/userSettingsResource.js +376 -353
  25. package/templates/packages/users/package.descriptor.mjs +4 -5
  26. package/templates/packages/users/package.json +0 -1
  27. package/templates/packages/users/src/server/UsersProvider.js +23 -24
  28. package/templates/packages/users/src/server/actions.js +26 -28
  29. package/templates/packages/users/src/server/registerRoutes.js +29 -15
  30. package/templates/packages/users/src/server/repository.js +35 -28
  31. package/templates/packages/users/src/server/service.js +20 -15
  32. package/templates/packages/users/src/shared/userResource.js +55 -68
  33. package/templates/packages/users-workspace/package.descriptor.mjs +4 -5
  34. package/templates/packages/users-workspace/src/server/UsersProvider.js +23 -24
  35. package/templates/packages/users-workspace/src/server/actions.js +28 -28
  36. package/templates/packages/users-workspace/src/server/registerRoutes.js +34 -16
  37. package/test/accountSecurityService.test.js +32 -0
  38. package/test/providerLifecycle.test.js +63 -0
  39. package/test/registerCommonRepositories.test.js +28 -8
  40. package/test/repositoryContracts.test.js +177 -28
  41. package/test/resourcesCanonical.test.js +18 -11
  42. package/test/userSettingsInternalResource.test.js +8 -0
  43. package/test/userSettingsResource.test.js +24 -7
  44. package/test/usersBootstrapContributor.test.js +40 -1
  45. package/test/usersPackageScaffoldContract.test.js +70 -3
  46. package/test/usersRouteRequestInputValidator.test.js +92 -23
  47. package/test/usersRouteResources.test.js +28 -18
  48. package/src/server/common/resources/userProfilesResource.js +0 -203
  49. package/src/server/common/validators/authenticatedUserValidator.js +0 -43
  50. package/src/shared/resources/resolveGlobalArrayRegistry.js +0 -6
  51. package/src/shared/resources/userSettingsFields.js +0 -76
  52. package/templates/packages/main/src/shared/resources/userSettingsFields.js +0 -138
  53. package/templates/packages/users/src/server/actionIds.js +0 -6
  54. package/templates/packages/users/src/server/listConfig.js +0 -16
  55. package/test/settingsFieldRegistriesSingleton.test.js +0 -14
  56. package/test-support/registerDefaultSettingsFields.js +0 -2
@@ -1,68 +1,68 @@
1
- import { recordIdParamsValidator } from "@jskit-ai/kernel/shared/validators";
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(LIST_CONFIG);
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 requireActionSurface(surface = "") {
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: actionIds.list,
22
+ id: "crud.users.list",
31
23
  version: 1,
32
24
  kind: "query",
33
25
  channels: ["api", "automation", "internal"],
34
- surfaces: [actionSurface],
26
+ surfaces: [surface],
35
27
  permission: authenticatedPermission,
36
- inputValidator: [workspaceSlugParamsValidator, listCursorPaginationQueryValidator, listSearchQueryValidator],
37
- outputValidator: resource.operations.list.outputValidator,
28
+ input: composeSchemaDefinitions([
29
+ workspaceSlugParamsValidator,
30
+ listCursorPaginationQueryValidator,
31
+ listSearchQueryValidator
32
+ ]),
33
+ output: null,
38
34
  idempotency: "none",
39
35
  audit: {
40
- actionName: actionIds.list
36
+ actionName: "crud.users.list"
41
37
  },
42
38
  observability: {},
43
39
  async execute(input, context, deps) {
44
- return deps.usersService.listRecords(input, {
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: actionIds.view,
48
+ id: "crud.users.view",
52
49
  version: 1,
53
50
  kind: "query",
54
51
  channels: ["api", "automation", "internal"],
55
- surfaces: [actionSurface],
52
+ surfaces: [surface],
56
53
  permission: authenticatedPermission,
57
- inputValidator: [workspaceSlugParamsValidator, recordIdParamsValidator],
58
- outputValidator: resource.operations.view.outputValidator,
54
+ input: composeSchemaDefinitions([
55
+ workspaceSlugParamsValidator,
56
+ recordIdParamsValidator
57
+ ]),
58
+ output: null,
59
59
  idempotency: "none",
60
60
  audit: {
61
- actionName: actionIds.view
61
+ actionName: "crud.users.view"
62
62
  },
63
63
  observability: {},
64
64
  async execute(input, context, deps) {
65
- return deps.usersService.getRecord(input.recordId, {
65
+ return deps.usersService.getDocumentById(input.recordId, {
66
66
  context,
67
67
  visibilityContext: context?.visibilityContext
68
68
  });
@@ -1,4 +1,4 @@
1
- import { withStandardErrorResponses } from "@jskit-ai/http-runtime/shared/validators/errorResponses";
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 { recordIdParamsValidator } from "@jskit-ai/kernel/shared/validators";
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(LIST_CONFIG);
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
- paramsValidator: routeParamsValidator,
47
- queryValidator: [listCursorPaginationQueryValidator, listSearchQueryValidator],
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: actionIds.list,
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
- paramsValidator: [routeParamsValidator, recordIdParamsValidator],
76
- responseValidators: withStandardErrorResponses({
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: actionIds.view,
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
- assert.equal(token, "jskit.database.knex");
22
- return Object.assign(() => {
23
- throw new Error("query execution not expected");
24
- }, {
25
- async transaction(work) {
26
- return work({ trxId: "trx-1" });
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 repositories = [createUserProfilesRepository(knex), createUserSettingsRepository(knex)];
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 createFindByEmailKnexStub(expectedRow) {
47
+ function createUserProfilesApiStub(expectedRecord) {
30
48
  const calls = [];
31
- const knex = Object.assign((tableName) => {
32
- assert.equal(tableName, "users");
33
- return {
34
- where(criteria) {
35
- calls.push(criteria);
36
- return {
37
- async first() {
38
- return expectedRow;
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
- return { knex, calls };
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 { knex, calls } = createFindByEmailKnexStub({
196
+ const { api, calls } = createUserProfilesApiStub({
54
197
  id: 7,
55
- auth_provider: "supabase",
56
- auth_provider_user_sid: "supabase-user-7",
198
+ authProvider: "supabase",
199
+ authProviderUserSid: "supabase-user-7",
57
200
  email: "ada@example.com",
58
201
  username: "ada",
59
- display_name: "Ada Example",
60
- avatar_storage_key: null,
61
- avatar_version: null,
62
- avatar_updated_at: null,
63
- created_at: "2026-04-20T00:00:00.000Z"
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 { knex } = createFindByEmailKnexStub(undefined);
77
- const repository = createUserProfilesRepository(knex);
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 "../test-support/registerDefaultSettingsFields.js";
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(typeof operation?.outputValidator?.schema, "object", `${label}.outputValidator.schema must exist.`);
55
- if (operation?.bodyValidator) {
56
- assert.equal(typeof operation.bodyValidator.schema, "object", `${label}.bodyValidator.schema must exist.`);
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?.paramsValidator) {
59
- assert.equal(typeof operation.paramsValidator.schema, "object", `${label}.paramsValidator.schema must exist.`);
65
+ if (operation?.params) {
66
+ assert.equal(typeof operation.params.schema, "object", `${label}.params.schema must exist.`);
60
67
  }
61
- if (operation?.queryValidator) {
62
- assert.equal(typeof operation.queryValidator.schema, "object", `${label}.queryValidator.schema must exist.`);
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 no longer contains legacy shared/schema directory", () => {
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 legacySchemaDir = path.join(packageRoot, "src", "shared", "schema");
71
- assert.equal(existsSync(legacySchemaDir), false, "src/shared/schema must not exist.");
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 "../test-support/registerDefaultSettingsFields.js";
5
- import { userSettingsResource } from "../src/shared/resources/userSettingsResource.js";
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: "bodyValidator",
12
+ section: "body",
11
13
  value: payload
12
14
  });
13
15
  }
14
16
 
15
- test("user settings preferences update keeps required string validation after normalization", () => {
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
+ });