@jskit-ai/users-core 0.1.55 → 0.1.57
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 +175 -6
- package/package.json +6 -6
- package/src/server/accountNotifications/accountNotificationsService.js +3 -3
- package/src/server/accountNotifications/registerAccountNotifications.js +2 -2
- package/src/server/accountPreferences/accountPreferencesService.js +3 -3
- package/src/server/accountPreferences/registerAccountPreferences.js +2 -2
- package/src/server/accountProfile/accountProfileService.js +5 -5
- package/src/server/accountProfile/avatarService.js +6 -6
- package/src/server/accountProfile/registerAccountProfile.js +3 -3
- package/src/server/accountSecurity/accountSecurityService.js +3 -3
- package/src/server/accountSecurity/registerAccountSecurity.js +2 -2
- package/src/server/common/registerCommonRepositories.js +5 -5
- package/src/server/common/repositories/{usersRepository.js → userProfilesRepository.js} +117 -95
- package/src/server/common/repositories/userSettingsRepository.js +12 -11
- package/src/server/common/resources/userProfilesResource.js +203 -0
- package/src/server/common/services/accountContextService.js +4 -4
- package/src/server/common/services/authProfileSyncService.js +11 -11
- package/src/server/common/support/identity.js +17 -0
- package/src/server/registerUsersBootstrap.js +2 -2
- package/src/server/registerUsersCore.js +2 -2
- package/src/server/usersBootstrapContributor.js +5 -5
- package/src/shared/resources/userProfileResource.js +1 -1
- package/src/shared/resources/userSettingsFields.js +10 -4
- package/src/shared/resources/userSettingsResource.js +1 -1
- package/templates/packages/main/src/shared/resources/userSettingsFields.js +10 -10
- package/templates/packages/users/package.descriptor.mjs +65 -0
- package/templates/packages/users/package.json +10 -0
- package/templates/packages/users/src/server/UsersProvider.js +91 -0
- package/templates/packages/users/src/server/actionIds.js +6 -0
- package/templates/packages/users/src/server/actions.js +73 -0
- package/templates/packages/users/src/server/listConfig.js +16 -0
- package/templates/packages/users/src/server/registerRoutes.js +87 -0
- package/templates/packages/users/src/server/repository.js +41 -0
- package/templates/packages/users/src/server/service.js +28 -0
- package/templates/packages/users/src/shared/index.js +1 -0
- package/templates/packages/users/src/shared/userResource.js +74 -0
- package/templates/packages/users-workspace/package.descriptor.mjs +66 -0
- package/templates/packages/users-workspace/src/server/UsersProvider.js +92 -0
- package/templates/packages/users-workspace/src/server/actions.js +74 -0
- package/templates/packages/users-workspace/src/server/registerRoutes.js +93 -0
- package/test/authProfileSyncService.test.js +3 -3
- package/test/avatarService.test.js +2 -2
- package/test/registerCommonRepositories.test.js +37 -0
- package/test/repositoryContracts.test.js +48 -5
- package/test/usersBootstrapContributor.test.js +2 -2
- package/test/usersPackageScaffoldContract.test.js +98 -0
- package/test/usersRouteResources.test.js +1 -1
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export default Object.freeze({
|
|
2
|
+
packageVersion: 1,
|
|
3
|
+
packageId: "@local/users",
|
|
4
|
+
version: "0.1.0",
|
|
5
|
+
kind: "runtime",
|
|
6
|
+
description: "App-local CRUD package (users).",
|
|
7
|
+
dependsOn: [
|
|
8
|
+
"@jskit-ai/auth-core",
|
|
9
|
+
"@jskit-ai/crud-core",
|
|
10
|
+
"@jskit-ai/database-runtime",
|
|
11
|
+
"@jskit-ai/http-runtime",
|
|
12
|
+
"@jskit-ai/users-core",
|
|
13
|
+
"@jskit-ai/workspaces-core"
|
|
14
|
+
],
|
|
15
|
+
capabilities: {
|
|
16
|
+
provides: [
|
|
17
|
+
"crud.users"
|
|
18
|
+
],
|
|
19
|
+
requires: [
|
|
20
|
+
"runtime.actions",
|
|
21
|
+
"runtime.database",
|
|
22
|
+
"auth.policy"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
runtime: {
|
|
26
|
+
server: {
|
|
27
|
+
providers: [
|
|
28
|
+
{
|
|
29
|
+
entrypoint: "src/server/UsersProvider.js",
|
|
30
|
+
export: "UsersProvider"
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
metadata: {
|
|
36
|
+
apiSummary: {
|
|
37
|
+
surfaces: [
|
|
38
|
+
{
|
|
39
|
+
subpath: "./server/actionIds",
|
|
40
|
+
summary: "App-local CRUD public action identifiers."
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
subpath: "./shared",
|
|
44
|
+
summary: "App-local CRUD shared resource."
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
containerTokens: {
|
|
48
|
+
server: [
|
|
49
|
+
"repository.users",
|
|
50
|
+
"crud.users"
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
mutations: {
|
|
56
|
+
dependencies: {
|
|
57
|
+
runtime: {},
|
|
58
|
+
dev: {}
|
|
59
|
+
},
|
|
60
|
+
packageJson: {
|
|
61
|
+
scripts: {}
|
|
62
|
+
},
|
|
63
|
+
procfile: {},
|
|
64
|
+
files: []
|
|
65
|
+
}
|
|
66
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { resolveAppConfig } from "@jskit-ai/kernel/server/support";
|
|
2
|
+
import { resolveCrudSurfacePolicyFromAppConfig } from "@jskit-ai/crud-core/server/crudModuleConfig";
|
|
3
|
+
import {
|
|
4
|
+
createCrudLookupResolver,
|
|
5
|
+
createCrudLookup
|
|
6
|
+
} from "@jskit-ai/crud-core/server/lookups";
|
|
7
|
+
import { withActionDefaults } from "@jskit-ai/kernel/shared/actions";
|
|
8
|
+
import { createRepository } from "./repository.js";
|
|
9
|
+
import {
|
|
10
|
+
createService,
|
|
11
|
+
serviceEvents
|
|
12
|
+
} from "./service.js";
|
|
13
|
+
import { createActions } from "./actions.js";
|
|
14
|
+
import { registerRoutes } from "./registerRoutes.js";
|
|
15
|
+
|
|
16
|
+
const CRUD_MODULE_CONFIG = Object.freeze({
|
|
17
|
+
namespace: "users",
|
|
18
|
+
surface: "admin",
|
|
19
|
+
ownershipFilter: "public",
|
|
20
|
+
relativePath: "/users"
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
function resolveCrudPolicyFromApp(app) {
|
|
24
|
+
return resolveCrudSurfacePolicyFromAppConfig(CRUD_MODULE_CONFIG, resolveAppConfig(app), {
|
|
25
|
+
context: "UsersProvider"
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class UsersProvider {
|
|
30
|
+
static id = "crud.users";
|
|
31
|
+
|
|
32
|
+
static dependsOn = ["runtime.actions", "runtime.database", "auth.policy.fastify", "local.main"];
|
|
33
|
+
|
|
34
|
+
register(app) {
|
|
35
|
+
if (!app || typeof app.singleton !== "function" || typeof app.service !== "function" || typeof app.actions !== "function") {
|
|
36
|
+
throw new Error("UsersProvider requires application singleton()/service()/actions().");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const crudPolicy = resolveCrudPolicyFromApp(app);
|
|
40
|
+
|
|
41
|
+
app.singleton("repository.users", (scope) => {
|
|
42
|
+
const knex = scope.make("jskit.database.knex");
|
|
43
|
+
return createRepository(knex, {
|
|
44
|
+
resolveLookup: createCrudLookupResolver(scope)
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
app.singleton("lookup.users", (scope) => {
|
|
49
|
+
return createCrudLookup(scope.make("repository.users"), {
|
|
50
|
+
ownershipFilter: crudPolicy.ownershipFilter
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
app.service(
|
|
55
|
+
"crud.users",
|
|
56
|
+
(scope) => {
|
|
57
|
+
return createService({
|
|
58
|
+
usersRepository: scope.make("repository.users")
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
events: serviceEvents
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
app.actions(
|
|
67
|
+
withActionDefaults(
|
|
68
|
+
createActions({
|
|
69
|
+
surface: crudPolicy.surfaceId
|
|
70
|
+
}),
|
|
71
|
+
{
|
|
72
|
+
domain: "crud",
|
|
73
|
+
dependencies: {
|
|
74
|
+
usersService: "crud.users"
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
boot(app) {
|
|
82
|
+
const crudPolicy = resolveCrudPolicyFromApp(app);
|
|
83
|
+
registerRoutes(app, {
|
|
84
|
+
routeOwnershipFilter: crudPolicy.ownershipFilter,
|
|
85
|
+
routeSurface: crudPolicy.surfaceId,
|
|
86
|
+
routeSurfaceRequiresWorkspace: crudPolicy.surfaceDefinition.requiresWorkspace === true,
|
|
87
|
+
routeRelativePath: crudPolicy.relativePath
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export { UsersProvider };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { recordIdParamsValidator } from "@jskit-ai/kernel/shared/validators";
|
|
2
|
+
import {
|
|
3
|
+
createCrudCursorPaginationQueryValidator,
|
|
4
|
+
listSearchQueryValidator
|
|
5
|
+
} from "@jskit-ai/crud-core/server/listQueryValidators";
|
|
6
|
+
import { workspaceSlugParamsValidator } from "@jskit-ai/workspaces-core/server/validators/routeParamsValidator";
|
|
7
|
+
import { resource } from "../shared/userResource.js";
|
|
8
|
+
import { actionIds } from "./actionIds.js";
|
|
9
|
+
import { LIST_CONFIG } from "./listConfig.js";
|
|
10
|
+
|
|
11
|
+
const listCursorPaginationQueryValidator = createCrudCursorPaginationQueryValidator(LIST_CONFIG);
|
|
12
|
+
const authenticatedPermission = Object.freeze({
|
|
13
|
+
require: "authenticated"
|
|
14
|
+
});
|
|
15
|
+
|
|
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
|
+
|
|
28
|
+
return Object.freeze([
|
|
29
|
+
{
|
|
30
|
+
id: actionIds.list,
|
|
31
|
+
version: 1,
|
|
32
|
+
kind: "query",
|
|
33
|
+
channels: ["api", "automation", "internal"],
|
|
34
|
+
surfaces: [actionSurface],
|
|
35
|
+
permission: authenticatedPermission,
|
|
36
|
+
inputValidator: [workspaceSlugParamsValidator, listCursorPaginationQueryValidator, listSearchQueryValidator],
|
|
37
|
+
outputValidator: resource.operations.list.outputValidator,
|
|
38
|
+
idempotency: "none",
|
|
39
|
+
audit: {
|
|
40
|
+
actionName: actionIds.list
|
|
41
|
+
},
|
|
42
|
+
observability: {},
|
|
43
|
+
async execute(input, context, deps) {
|
|
44
|
+
return deps.usersService.listRecords(input, {
|
|
45
|
+
context,
|
|
46
|
+
visibilityContext: context?.visibilityContext
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: actionIds.view,
|
|
52
|
+
version: 1,
|
|
53
|
+
kind: "query",
|
|
54
|
+
channels: ["api", "automation", "internal"],
|
|
55
|
+
surfaces: [actionSurface],
|
|
56
|
+
permission: authenticatedPermission,
|
|
57
|
+
inputValidator: [workspaceSlugParamsValidator, recordIdParamsValidator],
|
|
58
|
+
outputValidator: resource.operations.view.outputValidator,
|
|
59
|
+
idempotency: "none",
|
|
60
|
+
audit: {
|
|
61
|
+
actionName: actionIds.view
|
|
62
|
+
},
|
|
63
|
+
observability: {},
|
|
64
|
+
async execute(input, context, deps) {
|
|
65
|
+
return deps.usersService.getRecord(input.recordId, {
|
|
66
|
+
context,
|
|
67
|
+
visibilityContext: context?.visibilityContext
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
]);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export { createActions };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { withStandardErrorResponses } from "@jskit-ai/http-runtime/shared/validators/errorResponses";
|
|
2
|
+
import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
|
|
3
|
+
import {
|
|
4
|
+
createCrudCursorPaginationQueryValidator,
|
|
5
|
+
listSearchQueryValidator
|
|
6
|
+
} from "@jskit-ai/crud-core/server/listQueryValidators";
|
|
7
|
+
import { resolveScopedApiBasePath } from "@jskit-ai/kernel/shared/surface";
|
|
8
|
+
import { checkRouteVisibility } from "@jskit-ai/kernel/shared/support/visibility";
|
|
9
|
+
import { recordIdParamsValidator } from "@jskit-ai/kernel/shared/validators";
|
|
10
|
+
import { routeParamsValidator } from "@jskit-ai/workspaces-core/server/validators/routeParamsValidator";
|
|
11
|
+
import { buildWorkspaceInputFromRouteParams } from "@jskit-ai/workspaces-core/server/support/workspaceRouteInput";
|
|
12
|
+
import { actionIds } from "./actionIds.js";
|
|
13
|
+
import { LIST_CONFIG } from "./listConfig.js";
|
|
14
|
+
import { resource } from "../shared/userResource.js";
|
|
15
|
+
|
|
16
|
+
const listCursorPaginationQueryValidator = createCrudCursorPaginationQueryValidator(LIST_CONFIG);
|
|
17
|
+
|
|
18
|
+
function registerRoutes(
|
|
19
|
+
app,
|
|
20
|
+
{
|
|
21
|
+
routeOwnershipFilter = "public",
|
|
22
|
+
routeSurface = "",
|
|
23
|
+
routeSurfaceRequiresWorkspace = false,
|
|
24
|
+
routeRelativePath = ""
|
|
25
|
+
} = {}
|
|
26
|
+
) {
|
|
27
|
+
const router = app.make("jskit.http.router");
|
|
28
|
+
const normalizedRouteSurface = normalizeSurfaceId(routeSurface);
|
|
29
|
+
const routeBase = resolveScopedApiBasePath({
|
|
30
|
+
routeBase: routeSurfaceRequiresWorkspace === true ? "/w/:workspaceSlug" : "/",
|
|
31
|
+
relativePath: routeRelativePath,
|
|
32
|
+
strictParams: false
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
router.register(
|
|
36
|
+
"GET",
|
|
37
|
+
routeBase,
|
|
38
|
+
{
|
|
39
|
+
auth: "required",
|
|
40
|
+
surface: normalizedRouteSurface,
|
|
41
|
+
visibility: checkRouteVisibility(routeOwnershipFilter),
|
|
42
|
+
meta: {
|
|
43
|
+
tags: ["crud"],
|
|
44
|
+
summary: "List users."
|
|
45
|
+
},
|
|
46
|
+
paramsValidator: routeParamsValidator,
|
|
47
|
+
queryValidator: [listCursorPaginationQueryValidator, listSearchQueryValidator],
|
|
48
|
+
responseValidators: withStandardErrorResponses({
|
|
49
|
+
200: resource.operations.list.outputValidator
|
|
50
|
+
})
|
|
51
|
+
},
|
|
52
|
+
async function (request, reply) {
|
|
53
|
+
const response = await request.executeAction({
|
|
54
|
+
actionId: actionIds.list,
|
|
55
|
+
input: {
|
|
56
|
+
...buildWorkspaceInputFromRouteParams(request.input.params),
|
|
57
|
+
...(request.input.query || {})
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
reply.code(200).send(response);
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
router.register(
|
|
65
|
+
"GET",
|
|
66
|
+
`${routeBase}/:recordId`,
|
|
67
|
+
{
|
|
68
|
+
auth: "required",
|
|
69
|
+
surface: normalizedRouteSurface,
|
|
70
|
+
visibility: checkRouteVisibility(routeOwnershipFilter),
|
|
71
|
+
meta: {
|
|
72
|
+
tags: ["crud"],
|
|
73
|
+
summary: "View a user."
|
|
74
|
+
},
|
|
75
|
+
paramsValidator: [routeParamsValidator, recordIdParamsValidator],
|
|
76
|
+
responseValidators: withStandardErrorResponses({
|
|
77
|
+
200: resource.operations.view.outputValidator
|
|
78
|
+
})
|
|
79
|
+
},
|
|
80
|
+
async function (request, reply) {
|
|
81
|
+
const response = await request.executeAction({
|
|
82
|
+
actionId: actionIds.view,
|
|
83
|
+
input: {
|
|
84
|
+
...buildWorkspaceInputFromRouteParams(request.input.params),
|
|
85
|
+
recordId: request.input.params.recordId
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
reply.code(200).send(response);
|
|
89
|
+
}
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export { registerRoutes };
|
|
@@ -7,7 +7,7 @@ test("authProfileSyncService.syncIdentityProfile uses shared transaction for pro
|
|
|
7
7
|
const transaction = { trxId: "tx-1" };
|
|
8
8
|
|
|
9
9
|
const service = createService({
|
|
10
|
-
|
|
10
|
+
userProfilesRepository: {
|
|
11
11
|
async findByIdentity(_identity, options = {}) {
|
|
12
12
|
calls.push({ step: "find", trx: options.trx || null });
|
|
13
13
|
return null;
|
|
@@ -68,7 +68,7 @@ test("authProfileSyncService.syncIdentityProfile skips write path when profile i
|
|
|
68
68
|
let provisionCalls = 0;
|
|
69
69
|
|
|
70
70
|
const service = createService({
|
|
71
|
-
|
|
71
|
+
userProfilesRepository: {
|
|
72
72
|
async findByIdentity() {
|
|
73
73
|
return {
|
|
74
74
|
id: "7",
|
|
@@ -118,7 +118,7 @@ test("authProfileSyncService.syncIdentityProfile skips write path when profile i
|
|
|
118
118
|
test("authProfileSyncService.findByIdentity normalizes provider identity input", async () => {
|
|
119
119
|
let capturedIdentity = null;
|
|
120
120
|
const service = createService({
|
|
121
|
-
|
|
121
|
+
userProfilesRepository: {
|
|
122
122
|
async findByIdentity(identity) {
|
|
123
123
|
capturedIdentity = identity;
|
|
124
124
|
return null;
|
|
@@ -59,7 +59,7 @@ test("avatarService uploadForUser stores bytes and updates profile avatar fields
|
|
|
59
59
|
};
|
|
60
60
|
|
|
61
61
|
const avatarService = createService({
|
|
62
|
-
|
|
62
|
+
userProfilesRepository: repository,
|
|
63
63
|
avatarStorageService
|
|
64
64
|
});
|
|
65
65
|
|
|
@@ -99,7 +99,7 @@ test("avatarService clearForUser removes stored avatar and clears profile fields
|
|
|
99
99
|
};
|
|
100
100
|
|
|
101
101
|
const avatarService = createService({
|
|
102
|
-
|
|
102
|
+
userProfilesRepository: repository,
|
|
103
103
|
avatarStorageService
|
|
104
104
|
});
|
|
105
105
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { registerCommonRepositories } from "../src/server/common/registerCommonRepositories.js";
|
|
4
|
+
|
|
5
|
+
test("registerCommonRepositories exposes the shared users-core repositories", async () => {
|
|
6
|
+
const bindings = new Map();
|
|
7
|
+
const app = {
|
|
8
|
+
singleton(token, factory) {
|
|
9
|
+
bindings.set(token, factory);
|
|
10
|
+
return this;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
registerCommonRepositories(app);
|
|
15
|
+
|
|
16
|
+
assert.equal(typeof bindings.get("internal.repository.user-settings"), "function");
|
|
17
|
+
assert.equal(typeof bindings.get("internal.repository.user-profiles"), "function");
|
|
18
|
+
|
|
19
|
+
const scope = {
|
|
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
|
+
});
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const settingsRepository = bindings.get("internal.repository.user-settings")(scope);
|
|
33
|
+
const profileRepository = bindings.get("internal.repository.user-profiles")(scope);
|
|
34
|
+
|
|
35
|
+
assert.equal(typeof settingsRepository.findByUserId, "function");
|
|
36
|
+
assert.equal(typeof profileRepository.findById, "function");
|
|
37
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
-
import { createRepository as
|
|
3
|
+
import { createRepository as createUserProfilesRepository } from "../src/server/common/repositories/userProfilesRepository.js";
|
|
4
4
|
import { createRepository as createUserSettingsRepository } from "../src/server/common/repositories/userSettingsRepository.js";
|
|
5
5
|
|
|
6
6
|
function createKnexStub() {
|
|
@@ -17,10 +17,7 @@ function createKnexStub() {
|
|
|
17
17
|
|
|
18
18
|
test("users-core repositories expose withTransaction", async () => {
|
|
19
19
|
const knex = createKnexStub();
|
|
20
|
-
const repositories = [
|
|
21
|
-
createUsersRepository(knex),
|
|
22
|
-
createUserSettingsRepository(knex)
|
|
23
|
-
];
|
|
20
|
+
const repositories = [createUserProfilesRepository(knex), createUserSettingsRepository(knex)];
|
|
24
21
|
|
|
25
22
|
for (const repository of repositories) {
|
|
26
23
|
assert.equal(typeof repository.withTransaction, "function");
|
|
@@ -28,3 +25,49 @@ test("users-core repositories expose withTransaction", async () => {
|
|
|
28
25
|
assert.deepEqual(result, { id: "trx-1" });
|
|
29
26
|
}
|
|
30
27
|
});
|
|
28
|
+
|
|
29
|
+
function createFindByEmailKnexStub(expectedRow) {
|
|
30
|
+
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;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}, {
|
|
44
|
+
async transaction(work) {
|
|
45
|
+
return work({ trxId: "trx-1" });
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return { knex, calls };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
test("userProfilesRepository.findByEmail normalizes email lookup", async () => {
|
|
53
|
+
const { knex, calls } = createFindByEmailKnexStub({
|
|
54
|
+
id: 7,
|
|
55
|
+
auth_provider: "supabase",
|
|
56
|
+
auth_provider_user_sid: "supabase-user-7",
|
|
57
|
+
email: "ada@example.com",
|
|
58
|
+
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"
|
|
64
|
+
});
|
|
65
|
+
const repository = createUserProfilesRepository(knex);
|
|
66
|
+
|
|
67
|
+
const profile = await repository.findByEmail(" ADA@EXAMPLE.COM ");
|
|
68
|
+
|
|
69
|
+
assert.deepEqual(calls, [{ email: "ada@example.com" }]);
|
|
70
|
+
assert.equal(profile?.id, "7");
|
|
71
|
+
assert.equal(profile?.email, "ada@example.com");
|
|
72
|
+
assert.equal(profile?.displayName, "Ada Example");
|
|
73
|
+
});
|
|
@@ -33,7 +33,7 @@ test("users bootstrap contributor exposes the generic authenticated bootstrap pa
|
|
|
33
33
|
const profile = createAuthenticatedProfile({ id: "12" });
|
|
34
34
|
const writtenSessions = [];
|
|
35
35
|
const contributor = createUsersBootstrapContributor({
|
|
36
|
-
|
|
36
|
+
userProfilesRepository: {
|
|
37
37
|
async findById() {
|
|
38
38
|
return profile;
|
|
39
39
|
}
|
|
@@ -103,7 +103,7 @@ test("users bootstrap contributor exposes the generic authenticated bootstrap pa
|
|
|
103
103
|
|
|
104
104
|
test("users bootstrap contributor emits anonymous bootstrap payload without workspace fields", async () => {
|
|
105
105
|
const contributor = createUsersBootstrapContributor({
|
|
106
|
-
|
|
106
|
+
userProfilesRepository: {
|
|
107
107
|
async findById() {
|
|
108
108
|
return null;
|
|
109
109
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import descriptor from "../package.descriptor.mjs";
|
|
7
|
+
|
|
8
|
+
const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const PACKAGE_ROOT = path.resolve(TEST_DIRECTORY, "..");
|
|
10
|
+
|
|
11
|
+
function readFileMutationById(id) {
|
|
12
|
+
return descriptor.mutations.files.find((entry) => entry.id === id) || null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
test("users-core installs the app-local users package scaffold", () => {
|
|
16
|
+
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");
|
|
18
|
+
|
|
19
|
+
const expectedFileIds = [
|
|
20
|
+
"users-core-users-package-json",
|
|
21
|
+
"users-core-users-package-descriptor-base",
|
|
22
|
+
"users-core-users-package-descriptor-workspace",
|
|
23
|
+
"users-core-users-provider-base",
|
|
24
|
+
"users-core-users-provider-workspace",
|
|
25
|
+
"users-core-users-action-ids",
|
|
26
|
+
"users-core-users-actions-base",
|
|
27
|
+
"users-core-users-actions-workspace",
|
|
28
|
+
"users-core-users-list-config",
|
|
29
|
+
"users-core-users-routes-base",
|
|
30
|
+
"users-core-users-routes-workspace",
|
|
31
|
+
"users-core-users-repository",
|
|
32
|
+
"users-core-users-service",
|
|
33
|
+
"users-core-users-shared-index",
|
|
34
|
+
"users-core-users-resource"
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
for (const fileId of expectedFileIds) {
|
|
38
|
+
const mutation = readFileMutationById(fileId);
|
|
39
|
+
assert.ok(mutation, `Missing users-core scaffold file mutation ${fileId}.`);
|
|
40
|
+
assert.equal(mutation.ownership, "app", `${fileId} must remain app-owned.`);
|
|
41
|
+
assert.equal(mutation.preserveOnRemove, true, `${fileId} must remain preserved on remove.`);
|
|
42
|
+
assert.ok(mutation.to.startsWith("packages/users/"), `${fileId} must target packages/users.`);
|
|
43
|
+
assert.ok(mutation.from, `${fileId} must define a template source.`);
|
|
44
|
+
assert.ok(mutation.reason, `${fileId} must document why it exists.`);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("users-core base users package templates stay aligned with non-workspace apps", async () => {
|
|
49
|
+
const packageDescriptorSource = await readFile(
|
|
50
|
+
path.join(PACKAGE_ROOT, "templates/packages/users/package.descriptor.mjs"),
|
|
51
|
+
"utf8"
|
|
52
|
+
);
|
|
53
|
+
const providerSource = await readFile(
|
|
54
|
+
path.join(PACKAGE_ROOT, "templates/packages/users/src/server/UsersProvider.js"),
|
|
55
|
+
"utf8"
|
|
56
|
+
);
|
|
57
|
+
const actionsSource = await readFile(
|
|
58
|
+
path.join(PACKAGE_ROOT, "templates/packages/users/src/server/actions.js"),
|
|
59
|
+
"utf8"
|
|
60
|
+
);
|
|
61
|
+
const routesSource = await readFile(
|
|
62
|
+
path.join(PACKAGE_ROOT, "templates/packages/users/src/server/registerRoutes.js"),
|
|
63
|
+
"utf8"
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
assert.doesNotMatch(packageDescriptorSource, /@jskit-ai\/workspaces-core/);
|
|
67
|
+
assert.match(providerSource, /surface: "home"/);
|
|
68
|
+
assert.doesNotMatch(providerSource, /routeSurfaceRequiresWorkspace/);
|
|
69
|
+
assert.doesNotMatch(actionsSource, /workspaceSlugParamsValidator/);
|
|
70
|
+
assert.doesNotMatch(routesSource, /workspaceRouteInput/);
|
|
71
|
+
assert.match(routesSource, /routeBase: "\/"/);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("users-core workspace users package templates stay aligned with workspace apps", async () => {
|
|
75
|
+
const packageDescriptorSource = await readFile(
|
|
76
|
+
path.join(PACKAGE_ROOT, "templates/packages/users-workspace/package.descriptor.mjs"),
|
|
77
|
+
"utf8"
|
|
78
|
+
);
|
|
79
|
+
const providerSource = await readFile(
|
|
80
|
+
path.join(PACKAGE_ROOT, "templates/packages/users-workspace/src/server/UsersProvider.js"),
|
|
81
|
+
"utf8"
|
|
82
|
+
);
|
|
83
|
+
const actionsSource = await readFile(
|
|
84
|
+
path.join(PACKAGE_ROOT, "templates/packages/users-workspace/src/server/actions.js"),
|
|
85
|
+
"utf8"
|
|
86
|
+
);
|
|
87
|
+
const routesSource = await readFile(
|
|
88
|
+
path.join(PACKAGE_ROOT, "templates/packages/users-workspace/src/server/registerRoutes.js"),
|
|
89
|
+
"utf8"
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
assert.match(packageDescriptorSource, /@jskit-ai\/workspaces-core/);
|
|
93
|
+
assert.match(providerSource, /surface: "admin"/);
|
|
94
|
+
assert.match(providerSource, /routeSurfaceRequiresWorkspace/);
|
|
95
|
+
assert.match(actionsSource, /workspaceSlugParamsValidator/);
|
|
96
|
+
assert.match(routesSource, /buildWorkspaceInputFromRouteParams/);
|
|
97
|
+
assert.match(routesSource, /routeBase: routeSurfaceRequiresWorkspace === true \? "\/w\/:workspaceSlug" : "\/"/);
|
|
98
|
+
});
|
|
@@ -11,7 +11,7 @@ import { userSettingsResource } from "../src/shared/resources/userSettingsResour
|
|
|
11
11
|
function assertResourceShape(resource, label) {
|
|
12
12
|
assert.ok(resource, `${label} resource must exist.`);
|
|
13
13
|
assert.equal(typeof resource, "object", `${label} resource must be an object.`);
|
|
14
|
-
assert.equal(typeof resource.
|
|
14
|
+
assert.equal(typeof resource.namespace, "string", `.namespace must be a string.`);
|
|
15
15
|
|
|
16
16
|
for (const operationName of ["view", "list", "create", "replace", "patch"]) {
|
|
17
17
|
const operation = resource.operations?.[operationName];
|