@jskit-ai/users-core 0.1.65 → 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
@@ -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"], "0.1.54");
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 registeredRoutes = [];
33
- const router = {
34
- register(method, path, route, handler) {
35
- registeredRoutes.push({
36
- ...route,
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 registeredRoutes;
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(findRoute(routes, { method: "GET", path: "/api/settings" })?.path, "/api/settings");
94
- assert.equal(findRoute(routes, { method: "PATCH", path: "/api/settings/profile" })?.path, "/api/settings/profile");
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 { settings: {}, session: null };
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, { payload: { displayName: "Merc" } });
187
- assert.deepEqual(calls[1].input, { payload: { locale: "en-US" } });
188
- assert.deepEqual(calls[2].input, { payload: { email: true } });
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
- payload: {
191
- currentPassword: "old-password",
192
- newPassword: "new-password-123",
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, { payload: { enabled: true } });
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 "../test-support/registerDefaultSettingsFields.js";
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.outputValidator?.schema,
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.bodyValidator?.schema, "object", `${label}.operations.create.bodyValidator.schema is required.`);
37
- assert.equal(typeof resource.operations.replace.bodyValidator?.schema, "object", `${label}.operations.replace.bodyValidator.schema is required.`);
38
- assert.equal(typeof resource.operations.patch.bodyValidator?.schema, "object", `${label}.operations.patch.bodyValidator.schema is required.`);
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(typeof operation?.outputValidator?.schema, "object", `${label}.outputValidator.schema must exist.`);
71
- if (operation?.bodyValidator) {
72
- assert.equal(typeof operation.bodyValidator.schema, "object", `${label}.bodyValidator.schema must exist.`);
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?.paramsValidator) {
75
- assert.equal(typeof operation.paramsValidator.schema, "object", `${label}.paramsValidator.schema must exist.`);
84
+ if (operation?.params) {
85
+ assert.equal(typeof operation.params.schema, "object", `${label}.params.schema must exist.`);
76
86
  }
77
- if (operation?.queryValidator) {
78
- assert.equal(typeof operation.queryValidator.schema, "object", `${label}.queryValidator.schema must exist.`);
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 no longer uses a workspace schema helper that exposes raw schema leaves", () => {
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 legacyWorkspaceRoutesFile = path.join(packageRoot, "src", "server", "common", "routes", "workspaceRoutes.js");
87
- assert.equal(existsSync(legacyWorkspaceRoutesFile), false, "workspaceRoutes.js must not exist.");
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 no longer live under a legacy shared/schema directory", () => {
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 legacySchemaDir = path.join(packageRoot, "src", "shared", "schema");
94
- assert.equal(existsSync(legacySchemaDir), false, "src/shared/schema must not exist.");
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 };