@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,43 +0,0 @@
1
- import { Type } from "@fastify/type-provider-typebox";
2
- import { normalizeObjectInput, recordIdInputSchema } from "@jskit-ai/kernel/shared/validators";
3
- import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/actions/textNormalization";
4
- import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
5
-
6
- function normalizeAuthenticatedUser(input = {}) {
7
- const source = normalizeObjectInput(input);
8
- const id = normalizeRecordId(source.id, { fallback: null });
9
- if (!id) {
10
- return null;
11
- }
12
-
13
- const email = normalizeLowerText(source.email);
14
- return {
15
- id,
16
- email,
17
- username: normalizeLowerText(source.username),
18
- displayName: normalizeText(source.displayName) || email || `User ${id}`,
19
- authProvider: normalizeLowerText(source.authProvider),
20
- authProviderUserSid: normalizeText(source.authProviderUserSid),
21
- avatarStorageKey: source.avatarStorageKey ? normalizeText(source.avatarStorageKey) : null,
22
- avatarVersion: source.avatarVersion == null ? null : String(source.avatarVersion)
23
- };
24
- }
25
-
26
- const authenticatedUserValidator = Object.freeze({
27
- schema: Type.Object(
28
- {
29
- id: recordIdInputSchema,
30
- email: Type.String({ minLength: 1 }),
31
- username: Type.Optional(Type.String()),
32
- displayName: Type.Optional(Type.String()),
33
- authProvider: Type.Optional(Type.String()),
34
- authProviderUserSid: Type.Optional(Type.String()),
35
- avatarStorageKey: Type.Optional(Type.Union([Type.String(), Type.Null()])),
36
- avatarVersion: Type.Optional(Type.Union([Type.String(), Type.Number(), Type.Null()]))
37
- },
38
- { additionalProperties: true }
39
- ),
40
- normalize: normalizeAuthenticatedUser
41
- });
42
-
43
- export { authenticatedUserValidator };
@@ -1,6 +0,0 @@
1
- function resolveGlobalArrayRegistry(symbolKey) {
2
- globalThis[symbolKey] = Array.isArray(globalThis[symbolKey]) ? globalThis[symbolKey] : [];
3
- return globalThis[symbolKey];
4
- }
5
-
6
- export { resolveGlobalArrayRegistry };
@@ -1,76 +0,0 @@
1
- import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/actions/textNormalization";
2
- import { resolveGlobalArrayRegistry } from "./resolveGlobalArrayRegistry.js";
3
-
4
- const USER_SETTINGS_SECTIONS = Object.freeze({
5
- PREFERENCES: "preferences",
6
- NOTIFICATIONS: "notifications"
7
- });
8
-
9
- const USER_SETTINGS_SECTION_VALUES = Object.freeze(Object.values(USER_SETTINGS_SECTIONS));
10
-
11
- const userSettingsFields = resolveGlobalArrayRegistry("jskit.users-core.userSettingsFields");
12
-
13
- function defineField(field = {}) {
14
- const key = normalizeText(field.key);
15
- if (!key) {
16
- throw new TypeError("userSettingsFields.defineField requires field.key.");
17
- }
18
- if (userSettingsFields.some((entry) => entry.key === key)) {
19
- throw new Error(`userSettingsFields.defineField duplicate key: ${key}`);
20
- }
21
- if (!field.inputSchema || typeof field.inputSchema !== "object") {
22
- throw new TypeError(`userSettingsFields.defineField("${key}") requires inputSchema.`);
23
- }
24
- if (!field.outputSchema || typeof field.outputSchema !== "object") {
25
- throw new TypeError(`userSettingsFields.defineField("${key}") requires outputSchema.`);
26
- }
27
- const repository = field?.repository;
28
- if (!repository || typeof repository !== "object" || Array.isArray(repository)) {
29
- throw new TypeError(`userSettingsFields.defineField("${key}") requires repository.column.`);
30
- }
31
- const repositoryColumn = normalizeText(repository.column);
32
- if (!repositoryColumn) {
33
- throw new TypeError(`userSettingsFields.defineField("${key}") requires repository.column.`);
34
- }
35
- const section = normalizeLowerText(field.section);
36
- if (!USER_SETTINGS_SECTION_VALUES.includes(section)) {
37
- throw new TypeError(
38
- `userSettingsFields.defineField("${key}") requires section to be one of: ${USER_SETTINGS_SECTION_VALUES.join(", ")}.`
39
- );
40
- }
41
- if (typeof field.normalizeInput !== "function") {
42
- throw new TypeError(`userSettingsFields.defineField("${key}") requires normalizeInput.`);
43
- }
44
- if (typeof field.normalizeOutput !== "function") {
45
- throw new TypeError(`userSettingsFields.defineField("${key}") requires normalizeOutput.`);
46
- }
47
- if (typeof field.resolveDefault !== "function") {
48
- throw new TypeError(`userSettingsFields.defineField("${key}") requires resolveDefault.`);
49
- }
50
-
51
- userSettingsFields.push({
52
- key,
53
- section,
54
- repository: Object.freeze({
55
- column: repositoryColumn
56
- }),
57
- required: field.required !== false,
58
- includeInBootstrap: field.includeInBootstrap !== false,
59
- inputSchema: field.inputSchema,
60
- outputSchema: field.outputSchema,
61
- normalizeInput: field.normalizeInput,
62
- normalizeOutput: field.normalizeOutput,
63
- resolveDefault: field.resolveDefault
64
- });
65
- }
66
-
67
- function resetUserSettingsFields() {
68
- userSettingsFields.splice(0, userSettingsFields.length);
69
- }
70
-
71
- export {
72
- USER_SETTINGS_SECTIONS,
73
- defineField,
74
- resetUserSettingsFields,
75
- userSettingsFields
76
- };
@@ -1,138 +0,0 @@
1
- import { Type } from "typebox";
2
- import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/actions/textNormalization";
3
- import { DEFAULT_USER_SETTINGS } from "@jskit-ai/users-core/shared/settings";
4
- import {
5
- USER_SETTINGS_SECTIONS,
6
- defineField,
7
- resetUserSettingsFields
8
- } from "@jskit-ai/users-core/shared/resources/userSettingsFields";
9
-
10
- function normalizePositiveInteger(value, fallback) {
11
- const numericValue = Number(value);
12
- if (Number.isInteger(numericValue) && numericValue > 0) {
13
- return numericValue;
14
- }
15
- return Number(fallback);
16
- }
17
-
18
- resetUserSettingsFields();
19
-
20
- defineField({
21
- key: "theme",
22
- section: USER_SETTINGS_SECTIONS.PREFERENCES,
23
- repository: { column: "theme" },
24
- required: true,
25
- inputSchema: Type.String({ minLength: 1, maxLength: 32 }),
26
- outputSchema: Type.String({ minLength: 1, maxLength: 32 }),
27
- normalizeInput: (value) => normalizeText(value),
28
- normalizeOutput: (value) => normalizeText(value) || DEFAULT_USER_SETTINGS.theme,
29
- resolveDefault: () => DEFAULT_USER_SETTINGS.theme
30
- });
31
-
32
- defineField({
33
- key: "locale",
34
- section: USER_SETTINGS_SECTIONS.PREFERENCES,
35
- repository: { column: "locale" },
36
- required: true,
37
- inputSchema: Type.String({ minLength: 1, maxLength: 24 }),
38
- outputSchema: Type.String({ minLength: 1, maxLength: 24 }),
39
- normalizeInput: (value) => normalizeLowerText(value),
40
- normalizeOutput: (value) => normalizeLowerText(value) || DEFAULT_USER_SETTINGS.locale,
41
- resolveDefault: () => DEFAULT_USER_SETTINGS.locale
42
- });
43
-
44
- defineField({
45
- key: "timeZone",
46
- section: USER_SETTINGS_SECTIONS.PREFERENCES,
47
- repository: { column: "time_zone" },
48
- required: true,
49
- inputSchema: Type.String({ minLength: 1, maxLength: 64 }),
50
- outputSchema: Type.String({ minLength: 1, maxLength: 64 }),
51
- normalizeInput: (value) => normalizeText(value),
52
- normalizeOutput: (value) => normalizeText(value) || DEFAULT_USER_SETTINGS.timeZone,
53
- resolveDefault: () => DEFAULT_USER_SETTINGS.timeZone
54
- });
55
-
56
- defineField({
57
- key: "dateFormat",
58
- section: USER_SETTINGS_SECTIONS.PREFERENCES,
59
- repository: { column: "date_format" },
60
- required: true,
61
- inputSchema: Type.String({ minLength: 1, maxLength: 32 }),
62
- outputSchema: Type.String({ minLength: 1, maxLength: 32 }),
63
- normalizeInput: (value) => normalizeText(value),
64
- normalizeOutput: (value) => normalizeText(value) || DEFAULT_USER_SETTINGS.dateFormat,
65
- resolveDefault: () => DEFAULT_USER_SETTINGS.dateFormat
66
- });
67
-
68
- defineField({
69
- key: "numberFormat",
70
- section: USER_SETTINGS_SECTIONS.PREFERENCES,
71
- repository: { column: "number_format" },
72
- required: true,
73
- inputSchema: Type.String({ minLength: 1, maxLength: 32 }),
74
- outputSchema: Type.String({ minLength: 1, maxLength: 32 }),
75
- normalizeInput: (value) => normalizeText(value),
76
- normalizeOutput: (value) => normalizeText(value) || DEFAULT_USER_SETTINGS.numberFormat,
77
- resolveDefault: () => DEFAULT_USER_SETTINGS.numberFormat
78
- });
79
-
80
- defineField({
81
- key: "currencyCode",
82
- section: USER_SETTINGS_SECTIONS.PREFERENCES,
83
- repository: { column: "currency_code" },
84
- required: true,
85
- inputSchema: Type.String({ minLength: 3, maxLength: 3, pattern: "^[A-Za-z]{3}$" }),
86
- outputSchema: Type.String({ minLength: 3, maxLength: 3, pattern: "^[A-Z]{3}$" }),
87
- normalizeInput: (value) => normalizeText(value).toUpperCase(),
88
- normalizeOutput: (value) => normalizeText(value).toUpperCase() || DEFAULT_USER_SETTINGS.currencyCode,
89
- resolveDefault: () => DEFAULT_USER_SETTINGS.currencyCode
90
- });
91
-
92
- defineField({
93
- key: "avatarSize",
94
- section: USER_SETTINGS_SECTIONS.PREFERENCES,
95
- repository: { column: "avatar_size" },
96
- required: true,
97
- inputSchema: Type.Integer({ minimum: 1 }),
98
- outputSchema: Type.Integer({ minimum: 1 }),
99
- normalizeInput: (value) => Number(value),
100
- normalizeOutput: (value) => normalizePositiveInteger(value, DEFAULT_USER_SETTINGS.avatarSize),
101
- resolveDefault: () => DEFAULT_USER_SETTINGS.avatarSize
102
- });
103
-
104
- defineField({
105
- key: "productUpdates",
106
- section: USER_SETTINGS_SECTIONS.NOTIFICATIONS,
107
- repository: { column: "notify_product_updates" },
108
- required: true,
109
- inputSchema: Type.Boolean(),
110
- outputSchema: Type.Boolean(),
111
- normalizeInput: (value) => value,
112
- normalizeOutput: (value) => Boolean(value),
113
- resolveDefault: () => DEFAULT_USER_SETTINGS.productUpdates
114
- });
115
-
116
- defineField({
117
- key: "accountActivity",
118
- section: USER_SETTINGS_SECTIONS.NOTIFICATIONS,
119
- repository: { column: "notify_account_activity" },
120
- required: true,
121
- inputSchema: Type.Boolean(),
122
- outputSchema: Type.Boolean(),
123
- normalizeInput: (value) => value,
124
- normalizeOutput: (value) => Boolean(value),
125
- resolveDefault: () => DEFAULT_USER_SETTINGS.accountActivity
126
- });
127
-
128
- defineField({
129
- key: "securityAlerts",
130
- section: USER_SETTINGS_SECTIONS.NOTIFICATIONS,
131
- repository: { column: "notify_security_alerts" },
132
- required: true,
133
- inputSchema: Type.Boolean(),
134
- outputSchema: Type.Boolean(),
135
- normalizeInput: (value) => value,
136
- normalizeOutput: (value) => Boolean(value),
137
- resolveDefault: () => DEFAULT_USER_SETTINGS.securityAlerts
138
- });
@@ -1,6 +0,0 @@
1
- const actionIds = Object.freeze({
2
- list: "crud.users.list",
3
- view: "crud.users.view"
4
- });
5
-
6
- export { actionIds };
@@ -1,16 +0,0 @@
1
- const LIST_CONFIG = Object.freeze({
2
- searchColumns: ["display_name", "email", "username"],
3
- orderBy: [
4
- {
5
- column: "display_name",
6
- direction: "asc",
7
- nulls: "last"
8
- },
9
- {
10
- column: "email",
11
- direction: "asc"
12
- }
13
- ]
14
- });
15
-
16
- export { LIST_CONFIG };
@@ -1,14 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
-
4
- async function importWithIdentity(url, identity) {
5
- return import(`${url.href}?identity=${identity}`);
6
- }
7
-
8
- test("settings field registries stay shared across module identities", async () => {
9
- const userModuleUrl = new URL("../src/shared/resources/userSettingsFields.js", import.meta.url);
10
-
11
- const userA = await importWithIdentity(userModuleUrl, "user-a");
12
- const userB = await importWithIdentity(userModuleUrl, "user-b");
13
- assert.equal(userA.userSettingsFields, userB.userSettingsFields);
14
- });
@@ -1,2 +0,0 @@
1
- import "../../workspaces-core/templates/packages/main/src/shared/resources/workspaceSettingsFields.js";
2
- import "../templates/packages/main/src/shared/resources/userSettingsFields.js";