@friggframework/core 2.0.0--canary.684.0da024b.0 → 2.0.0--canary.679.a2bcc2f.0
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/CLAUDE.md +0 -2
- package/README.md +0 -105
- package/application/commands/integration-commands.js +2 -2
- package/application/commands/user-commands.js +5 -17
- package/credential/use-cases/credential-operations-for-user.js +25 -110
- package/credential/use-cases/reauthorize-credential-for-user.js +8 -15
- package/database/documentdb-utils.js +1 -6
- package/generated/prisma-mongodb/edge.js +4 -15
- package/generated/prisma-mongodb/index-browser.js +1 -12
- package/generated/prisma-mongodb/index.d.ts +86 -2027
- package/generated/prisma-mongodb/index.js +4 -15
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +0 -47
- package/generated/prisma-mongodb/wasm.js +4 -15
- package/generated/prisma-postgresql/edge.js +4 -15
- package/generated/prisma-postgresql/index-browser.js +1 -12
- package/generated/prisma-postgresql/index.d.ts +63 -2162
- package/generated/prisma-postgresql/index.js +4 -15
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +0 -44
- package/generated/prisma-postgresql/wasm.js +4 -15
- package/handlers/app-definition-loader.js +1 -15
- package/index.js +0 -18
- package/integrations/integration-router.js +73 -115
- package/integrations/repositories/integration-repository-documentdb.js +3 -35
- package/integrations/repositories/integration-repository-interface.js +1 -17
- package/integrations/repositories/integration-repository-mongo.js +76 -59
- package/integrations/repositories/integration-repository-postgres.js +76 -62
- package/integrations/tests/doubles/test-integration-repository.js +1 -12
- package/integrations/use-cases/create-integration.js +20 -55
- package/integrations/use-cases/get-integrations-for-user.js +40 -80
- package/integrations/use-cases/index.js +1 -5
- package/integrations/use-cases/integration-operations-for-user.js +24 -55
- package/management-api/management-api-config.js +0 -3
- package/management-api/openapi/components.js +0 -40
- package/management-api/openapi/v2-operations.js +1 -13
- package/management-api/v2/dependencies.js +15 -40
- package/management-api/v2/dto.js +0 -20
- package/management-api/v2/handlers/authorization.js +0 -1
- package/management-api/v2/handlers/credentials.js +2 -6
- package/management-api/v2/handlers/entities.js +7 -10
- package/management-api/v2/handlers/integrations.js +3 -6
- package/management-api/v2/request.js +1 -32
- package/modules/index.js +0 -8
- package/modules/repositories/module-repository-documentdb.js +4 -48
- package/modules/repositories/module-repository-interface.js +2 -19
- package/modules/repositories/module-repository-mongo.js +73 -65
- package/modules/repositories/module-repository-postgres.js +73 -64
- package/modules/use-cases/entity-operations-for-user.js +13 -50
- package/modules/use-cases/entity-types.js +7 -31
- package/modules/use-cases/execute-entity-proxy-request.js +0 -15
- package/modules/use-cases/get-entities-for-user.js +5 -16
- package/modules/use-cases/get-entity-module-for-user.js +3 -23
- package/modules/use-cases/get-entity-options-by-id.js +3 -9
- package/modules/use-cases/get-module.js +3 -9
- package/modules/use-cases/multi-step-authorization.js +12 -39
- package/modules/use-cases/process-authorization-callback.js +18 -52
- package/modules/use-cases/refresh-entity-options.js +3 -9
- package/modules/use-cases/test-module-auth.js +3 -9
- package/package.json +5 -6
- package/prisma-mongodb/schema.prisma +0 -47
- package/prisma-postgresql/schema.prisma +0 -44
- package/types/index.d.ts +0 -1
- package/types/modules/index.d.ts +0 -87
- package/user/repositories/user-repository-documentdb.js +2 -27
- package/user/repositories/user-repository-interface.js +0 -18
- package/user/repositories/user-repository-mongo.js +0 -24
- package/user/repositories/user-repository-postgres.js +0 -18
- package/user/use-cases/authenticate-user.js +1 -7
- package/user/use-cases/get-user-from-adopter-jwt.js +124 -95
- package/user/use-cases/get-user-from-x-frigg-headers.js +80 -83
- package/user/user.js +1 -64
- package/database/organization-where.js +0 -18
- package/database/unique-violation.js +0 -25
- package/integrations/repositories/integration-not-found.js +0 -14
- package/integrations/use-cases/resolve-integration-ownership.js +0 -177
- package/modules/entity-scope.js +0 -15
- package/modules/scope-config.js +0 -192
- package/modules/scope-policy.js +0 -112
- package/modules/scope-resolution.js +0 -288
- package/modules/scope-visibility.js +0 -239
- package/modules/tests/doubles/ownership-matching.js +0 -18
- package/modules/use-cases/authorize-entity-for-user.js +0 -61
- package/modules/use-cases/create-entity-from-credential.js +0 -113
- package/modules/use-cases/manage-access.js +0 -21
- package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +0 -26
- package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +0 -25
- package/types/user/index.d.ts +0 -134
- package/user/adopter-jwt-config.js +0 -177
- package/user/adopter-jwt-errors.js +0 -59
- package/user/adopter-token-verifier.js +0 -122
- package/user/app-user-helpers.js +0 -97
- package/user/create-user-authentication.js +0 -64
- package/user/membership.js +0 -51
- package/user/repositories/organization-membership-record.js +0 -19
- package/user/repositories/organization-membership-repository-documentdb.js +0 -106
- package/user/repositories/organization-membership-repository-factory.js +0 -42
- package/user/repositories/organization-membership-repository-interface.js +0 -57
- package/user/repositories/organization-membership-repository-mongo.js +0 -13
- package/user/repositories/organization-membership-repository-postgres.js +0 -29
- package/user/repositories/organization-membership-repository-prisma.js +0 -81
- package/user/tests/doubles/adopter-jwt-fixtures.js +0 -220
- package/user/use-cases/ensure-organization-membership.js +0 -92
- package/user/use-cases/find-individual-users-by-organization.js +0 -54
package/user/app-user-helpers.js
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
const { User } = require('./user');
|
|
2
|
-
const { isUniqueViolation } = require('../database/unique-violation');
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Find-or-create steps shared by the paths that identify a user by an app user
|
|
6
|
-
* id and/or app organization id (x-frigg headers, adopter token). Each path
|
|
7
|
-
* keeps its own policy between "find" and "create".
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
const wantsIndividual = (userConfig) =>
|
|
11
|
-
userConfig.individualUserRequired !== false;
|
|
12
|
-
|
|
13
|
-
const wantsOrganization = (userConfig, appOrgId) =>
|
|
14
|
-
Boolean(appOrgId) && Boolean(userConfig.organizationUserRequired);
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Runs `create`; if it loses a first-login race (a unique-constraint
|
|
18
|
-
* violation, see database/unique-violation.js), runs `find` once, since the
|
|
19
|
-
* winner has created the row by now. Anything else, and a violation with
|
|
20
|
-
* nothing to find, is rethrown.
|
|
21
|
-
*/
|
|
22
|
-
async function createOrFindOnRace(create, find) {
|
|
23
|
-
try {
|
|
24
|
-
return await create();
|
|
25
|
-
} catch (error) {
|
|
26
|
-
if (!isUniqueViolation(error)) throw error;
|
|
27
|
-
const found = await find();
|
|
28
|
-
if (!found) throw error;
|
|
29
|
-
return found;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async function findAppUsers(
|
|
34
|
-
userRepository,
|
|
35
|
-
userConfig,
|
|
36
|
-
{ appUserId, appOrgId }
|
|
37
|
-
) {
|
|
38
|
-
const individual =
|
|
39
|
-
appUserId && wantsIndividual(userConfig)
|
|
40
|
-
? await userRepository.findIndividualUserByAppUserId(appUserId)
|
|
41
|
-
: null;
|
|
42
|
-
const organization = wantsOrganization(userConfig, appOrgId)
|
|
43
|
-
? await userRepository.findOrganizationUserByAppOrgId(appOrgId)
|
|
44
|
-
: null;
|
|
45
|
-
return { individual, organization };
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Creates whichever of the two users is wanted and missing. Does not link
|
|
50
|
-
* them: linking is policy.
|
|
51
|
-
*/
|
|
52
|
-
async function createMissingAppUsers(
|
|
53
|
-
userRepository,
|
|
54
|
-
userConfig,
|
|
55
|
-
{ appUserId, appOrgId, individual, organization }
|
|
56
|
-
) {
|
|
57
|
-
if (!individual && appUserId && wantsIndividual(userConfig)) {
|
|
58
|
-
individual = await createOrFindOnRace(
|
|
59
|
-
() =>
|
|
60
|
-
userRepository.createIndividualUser({
|
|
61
|
-
appUserId,
|
|
62
|
-
username: `app-user-${appUserId}`,
|
|
63
|
-
email: `${appUserId}@app.local`,
|
|
64
|
-
}),
|
|
65
|
-
() => userRepository.findIndividualUserByAppUserId(appUserId)
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
if (!organization && wantsOrganization(userConfig, appOrgId)) {
|
|
69
|
-
// No race retry here: `appOrgId` has no unique index (MongoDB cannot
|
|
70
|
-
// express one that ignores individuals), so a lost race cannot raise a
|
|
71
|
-
// violation to recover from. Two simultaneous first logins for a new
|
|
72
|
-
// organization can still create it twice.
|
|
73
|
-
organization = await userRepository.createOrganizationUser({
|
|
74
|
-
appOrgId,
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
return { individual, organization };
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function buildUser(userConfig, individual, organization) {
|
|
81
|
-
return new User(
|
|
82
|
-
individual,
|
|
83
|
-
organization,
|
|
84
|
-
userConfig.usePassword,
|
|
85
|
-
userConfig.primary,
|
|
86
|
-
userConfig.individualUserRequired,
|
|
87
|
-
userConfig.organizationUserRequired
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
module.exports = {
|
|
92
|
-
wantsIndividual,
|
|
93
|
-
wantsOrganization,
|
|
94
|
-
findAppUsers,
|
|
95
|
-
createMissingAppUsers,
|
|
96
|
-
buildUser,
|
|
97
|
-
};
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
const {
|
|
2
|
-
createUserRepository,
|
|
3
|
-
} = require('./repositories/user-repository-factory');
|
|
4
|
-
const {
|
|
5
|
-
createOrganizationMembershipRepository,
|
|
6
|
-
} = require('./repositories/organization-membership-repository-factory');
|
|
7
|
-
const { createAdopterTokenVerifier } = require('./adopter-token-verifier');
|
|
8
|
-
const { GetUserFromBearerToken } = require('./use-cases/get-user-from-bearer-token');
|
|
9
|
-
const {
|
|
10
|
-
GetUserFromXFriggHeaders,
|
|
11
|
-
} = require('./use-cases/get-user-from-x-frigg-headers');
|
|
12
|
-
const { GetUserFromAdopterJwt } = require('./use-cases/get-user-from-adopter-jwt');
|
|
13
|
-
const {
|
|
14
|
-
EnsureOrganizationMembership,
|
|
15
|
-
} = require('./use-cases/ensure-organization-membership');
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Composition root for the three "who is this request" use cases, so the
|
|
19
|
-
* Lambda integration router and the Management API v2 wire them identically.
|
|
20
|
-
*
|
|
21
|
-
* @param {Object} params
|
|
22
|
-
* @param {Object} params.userConfig The `user` block of the app definition.
|
|
23
|
-
* @param {Object} [params.userRepository] Defaults to the configured database's.
|
|
24
|
-
* @param {Object} [params.organizationMembershipRepository] Defaults to the configured database's.
|
|
25
|
-
* @param {{verify: Function, claimNames: Object}} [params.tokenVerifier]
|
|
26
|
-
* Defaults to a verifier for `userConfig.jwtConfig`.
|
|
27
|
-
* @returns {{
|
|
28
|
-
* getUserFromBearerToken: GetUserFromBearerToken,
|
|
29
|
-
* getUserFromXFriggHeaders: GetUserFromXFriggHeaders,
|
|
30
|
-
* getUserFromAdopterJwt: GetUserFromAdopterJwt,
|
|
31
|
-
* }}
|
|
32
|
-
*/
|
|
33
|
-
function buildUserAuthentication({
|
|
34
|
-
userConfig,
|
|
35
|
-
userRepository = createUserRepository(),
|
|
36
|
-
organizationMembershipRepository = createOrganizationMembershipRepository(),
|
|
37
|
-
tokenVerifier = createAdopterTokenVerifier({
|
|
38
|
-
jwtConfig: userConfig?.jwtConfig,
|
|
39
|
-
}),
|
|
40
|
-
}) {
|
|
41
|
-
const ensureOrganizationMembership = new EnsureOrganizationMembership({
|
|
42
|
-
userRepository,
|
|
43
|
-
organizationMembershipRepository,
|
|
44
|
-
});
|
|
45
|
-
return {
|
|
46
|
-
getUserFromBearerToken: new GetUserFromBearerToken({
|
|
47
|
-
userRepository,
|
|
48
|
-
userConfig,
|
|
49
|
-
}),
|
|
50
|
-
getUserFromXFriggHeaders: new GetUserFromXFriggHeaders({
|
|
51
|
-
userRepository,
|
|
52
|
-
ensureOrganizationMembership,
|
|
53
|
-
userConfig,
|
|
54
|
-
}),
|
|
55
|
-
getUserFromAdopterJwt: new GetUserFromAdopterJwt({
|
|
56
|
-
userRepository,
|
|
57
|
-
ensureOrganizationMembership,
|
|
58
|
-
userConfig,
|
|
59
|
-
tokenVerifier,
|
|
60
|
-
}),
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
module.exports = { buildUserAuthentication };
|
package/user/membership.js
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Organization membership rules that are not persistence (ADR-056 §3.6). Pure.
|
|
3
|
-
*
|
|
4
|
-
* `IndividualUser.organizationId` is the legacy single-org pointer. It is not
|
|
5
|
-
* copied into membership rows; it is read as an implicit membership here, in
|
|
6
|
-
* the domain, so the repositories stay atomic (a row or nothing). See
|
|
7
|
-
* docs/guides/organization-memberships.md for why this is a read-time
|
|
8
|
-
* fallback rather than a data migration.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
/** The role a membership has when nothing says otherwise. */
|
|
12
|
-
const DEFAULT_ORG_ROLE = 'MEMBER';
|
|
13
|
-
|
|
14
|
-
const isPresent = (value) =>
|
|
15
|
-
value !== undefined && value !== null && value !== '';
|
|
16
|
-
|
|
17
|
-
const idString = (value) => (isPresent(value) ? String(value) : null);
|
|
18
|
-
|
|
19
|
-
/** Ids compare as strings: stores return numbers or strings. */
|
|
20
|
-
const sameId = (a, b) => isPresent(a) && isPresent(b) && String(a) === String(b);
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* The membership the legacy `organizationId` pointer stands for in
|
|
24
|
-
* `organizationUserId`, or null when the person has no pointer or it names a
|
|
25
|
-
* different organization.
|
|
26
|
-
*
|
|
27
|
-
* @param {Object|null} individual The individual user as the repository returns it.
|
|
28
|
-
* @param {string|number} organizationUserId
|
|
29
|
-
* @returns {{id: null, individualUserId: string|null, organizationUserId: string, orgRole: string, createdAt: *, implicit: true}|null}
|
|
30
|
-
*/
|
|
31
|
-
function implicitMembership(individual, organizationUserId) {
|
|
32
|
-
if (!individual || !sameId(individual.organizationId, organizationUserId)) {
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
return {
|
|
36
|
-
id: null,
|
|
37
|
-
individualUserId: idString(individual.id),
|
|
38
|
-
organizationUserId: idString(individual.organizationId),
|
|
39
|
-
orgRole: DEFAULT_ORG_ROLE,
|
|
40
|
-
createdAt: individual.createdAt,
|
|
41
|
-
implicit: true,
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
module.exports = {
|
|
46
|
-
DEFAULT_ORG_ROLE,
|
|
47
|
-
isPresent,
|
|
48
|
-
idString,
|
|
49
|
-
sameId,
|
|
50
|
-
implicitMembership,
|
|
51
|
-
};
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
const { idString } = require('../membership');
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* A stored membership row as the application sees it: every id a string
|
|
5
|
-
* whatever the database holds. `implicit` is always false here: only the
|
|
6
|
-
* domain (`user/membership.js`) produces an implicit membership.
|
|
7
|
-
*/
|
|
8
|
-
function toMembership(row) {
|
|
9
|
-
return {
|
|
10
|
-
id: idString(row.id ?? row._id),
|
|
11
|
-
individualUserId: idString(row.individualUserId),
|
|
12
|
-
organizationUserId: idString(row.organizationUserId),
|
|
13
|
-
orgRole: row.orgRole,
|
|
14
|
-
createdAt: row.createdAt,
|
|
15
|
-
implicit: false,
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
module.exports = { toMembership };
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
const { prisma } = require('../../database/prisma');
|
|
2
|
-
const {
|
|
3
|
-
toObjectId,
|
|
4
|
-
fromObjectId,
|
|
5
|
-
findOne,
|
|
6
|
-
findManyDrained,
|
|
7
|
-
updateOne,
|
|
8
|
-
} = require('../../database/documentdb-utils');
|
|
9
|
-
const { DUPLICATE_KEY_CODE } = require('../../database/unique-violation');
|
|
10
|
-
const {
|
|
11
|
-
OrganizationMembershipRepositoryInterface,
|
|
12
|
-
} = require('./organization-membership-repository-interface');
|
|
13
|
-
const { toMembership } = require('./organization-membership-record');
|
|
14
|
-
const { DEFAULT_ORG_ROLE } = require('../membership');
|
|
15
|
-
|
|
16
|
-
const COLLECTION = 'OrganizationMembership';
|
|
17
|
-
|
|
18
|
-
const parseDate = (value) => {
|
|
19
|
-
if (!value) return undefined;
|
|
20
|
-
const date = new Date(value.$date ?? value);
|
|
21
|
-
return Number.isNaN(date.getTime()) ? undefined : date;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Organization membership persistence for DocumentDB through raw commands.
|
|
26
|
-
* Nothing in a membership is encrypted.
|
|
27
|
-
*/
|
|
28
|
-
class OrganizationMembershipRepositoryDocumentDB extends OrganizationMembershipRepositoryInterface {
|
|
29
|
-
constructor() {
|
|
30
|
-
super();
|
|
31
|
-
this.prisma = prisma;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async findMembership(individualUserId, organizationUserId) {
|
|
35
|
-
const doc = await this._findDoc(individualUserId, organizationUserId);
|
|
36
|
-
return doc ? this._toMembership(doc) : null;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async addMembership(individualUserId, organizationUserId, options = {}) {
|
|
40
|
-
// $setOnInsert: an existing membership is left exactly as it is.
|
|
41
|
-
const result = await updateOne(
|
|
42
|
-
this.prisma,
|
|
43
|
-
COLLECTION,
|
|
44
|
-
this._pair(individualUserId, organizationUserId),
|
|
45
|
-
{
|
|
46
|
-
$setOnInsert: {
|
|
47
|
-
orgRole: options.orgRole ?? DEFAULT_ORG_ROLE,
|
|
48
|
-
createdAt: new Date(),
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
{ upsert: true }
|
|
52
|
-
);
|
|
53
|
-
// The loser of a first-login race can hit the unique pair; the row
|
|
54
|
-
// exists then, so only other write errors are failures.
|
|
55
|
-
const writeError = result?.writeErrors?.[0];
|
|
56
|
-
if (writeError && writeError.code !== DUPLICATE_KEY_CODE) {
|
|
57
|
-
throw new Error(
|
|
58
|
-
`Adding organization membership failed: ${writeError.errmsg} (code: ${writeError.code})`
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
const doc = await this._findDoc(individualUserId, organizationUserId);
|
|
62
|
-
if (!doc) {
|
|
63
|
-
throw new Error('Organization membership was not stored');
|
|
64
|
-
}
|
|
65
|
-
return this._toMembership(doc);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async listMembershipsForOrganization(organizationUserId) {
|
|
69
|
-
// Drained: findMany returns only the first cursor batch (~101 documents),
|
|
70
|
-
// which would silently drop members of a large organization.
|
|
71
|
-
const docs = await findManyDrained(
|
|
72
|
-
this.prisma,
|
|
73
|
-
COLLECTION,
|
|
74
|
-
{ organizationUserId: toObjectId(organizationUserId) },
|
|
75
|
-
{ sort: { createdAt: 1 } }
|
|
76
|
-
);
|
|
77
|
-
return docs.map((doc) => this._toMembership(doc));
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
_pair(individualUserId, organizationUserId) {
|
|
81
|
-
return {
|
|
82
|
-
individualUserId: toObjectId(individualUserId),
|
|
83
|
-
organizationUserId: toObjectId(organizationUserId),
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async _findDoc(individualUserId, organizationUserId) {
|
|
88
|
-
return findOne(
|
|
89
|
-
this.prisma,
|
|
90
|
-
COLLECTION,
|
|
91
|
-
this._pair(individualUserId, organizationUserId)
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
_toMembership(doc) {
|
|
96
|
-
return toMembership({
|
|
97
|
-
id: fromObjectId(doc._id),
|
|
98
|
-
individualUserId: fromObjectId(doc.individualUserId),
|
|
99
|
-
organizationUserId: fromObjectId(doc.organizationUserId),
|
|
100
|
-
orgRole: doc.orgRole ?? DEFAULT_ORG_ROLE,
|
|
101
|
-
createdAt: parseDate(doc.createdAt),
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
module.exports = { OrganizationMembershipRepositoryDocumentDB };
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
const databaseConfig = require('../../database/config');
|
|
2
|
-
const {
|
|
3
|
-
OrganizationMembershipRepositoryMongo,
|
|
4
|
-
} = require('./organization-membership-repository-mongo');
|
|
5
|
-
const {
|
|
6
|
-
OrganizationMembershipRepositoryPrisma,
|
|
7
|
-
} = require('./organization-membership-repository-prisma');
|
|
8
|
-
const {
|
|
9
|
-
OrganizationMembershipRepositoryPostgres,
|
|
10
|
-
} = require('./organization-membership-repository-postgres');
|
|
11
|
-
const {
|
|
12
|
-
OrganizationMembershipRepositoryDocumentDB,
|
|
13
|
-
} = require('./organization-membership-repository-documentdb');
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Membership repository for the configured database. Every method returns
|
|
17
|
-
* String ids whatever the database stores.
|
|
18
|
-
*
|
|
19
|
-
* @returns {import('./organization-membership-repository-interface').OrganizationMembershipRepositoryInterface}
|
|
20
|
-
*/
|
|
21
|
-
function createOrganizationMembershipRepository() {
|
|
22
|
-
switch (databaseConfig.DB_TYPE) {
|
|
23
|
-
case 'mongodb':
|
|
24
|
-
return new OrganizationMembershipRepositoryMongo();
|
|
25
|
-
case 'postgresql':
|
|
26
|
-
return new OrganizationMembershipRepositoryPostgres();
|
|
27
|
-
case 'documentdb':
|
|
28
|
-
return new OrganizationMembershipRepositoryDocumentDB();
|
|
29
|
-
default:
|
|
30
|
-
throw new Error(
|
|
31
|
-
`Unsupported DB_TYPE: ${databaseConfig.DB_TYPE}. Supported values: 'mongodb', 'documentdb', 'postgresql'`
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
module.exports = {
|
|
37
|
-
createOrganizationMembershipRepository,
|
|
38
|
-
OrganizationMembershipRepositoryPrisma,
|
|
39
|
-
OrganizationMembershipRepositoryMongo,
|
|
40
|
-
OrganizationMembershipRepositoryPostgres,
|
|
41
|
-
OrganizationMembershipRepositoryDocumentDB,
|
|
42
|
-
};
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Organization Membership Repository Interface (ADR-056 §3.6).
|
|
3
|
-
*
|
|
4
|
-
* Atomic persistence of "this person belongs to that organization": a row or
|
|
5
|
-
* nothing. No policy and no reading of `IndividualUser.organizationId`: the
|
|
6
|
-
* legacy pointer counts as an implicit membership, and that rule lives in the
|
|
7
|
-
* domain (`user/membership.js`), applied by the use cases.
|
|
8
|
-
*
|
|
9
|
-
* Every method returns String ids whatever the database stores.
|
|
10
|
-
*
|
|
11
|
-
* @abstract
|
|
12
|
-
*/
|
|
13
|
-
class OrganizationMembershipRepositoryInterface {
|
|
14
|
-
/**
|
|
15
|
-
* @param {string|number} individualUserId
|
|
16
|
-
* @param {string|number} organizationUserId
|
|
17
|
-
* @returns {Promise<Object|null>} The stored membership, or null.
|
|
18
|
-
* @abstract
|
|
19
|
-
*/
|
|
20
|
-
async findMembership(individualUserId, organizationUserId) {
|
|
21
|
-
throw new Error('Method findMembership must be implemented by subclass');
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Adds a membership if there is none; never changes an existing one.
|
|
26
|
-
* Idempotent, and safe against a concurrent identical call: both resolve
|
|
27
|
-
* to the same row.
|
|
28
|
-
*
|
|
29
|
-
* @param {string|number} individualUserId
|
|
30
|
-
* @param {string|number} organizationUserId
|
|
31
|
-
* @param {Object} [options]
|
|
32
|
-
* @param {string|null} [options.orgRole] Stored only when the row is
|
|
33
|
-
* created; defaults to `MEMBER`.
|
|
34
|
-
* @returns {Promise<Object>} The stored membership.
|
|
35
|
-
* @abstract
|
|
36
|
-
*/
|
|
37
|
-
async addMembership(individualUserId, organizationUserId, options) {
|
|
38
|
-
throw new Error('Method addMembership must be implemented by subclass');
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Stored memberships of an organization, oldest first. Rows only: people
|
|
43
|
-
* who are in the organization through the legacy pointer alone are not
|
|
44
|
-
* here.
|
|
45
|
-
*
|
|
46
|
-
* @param {string|number} organizationUserId
|
|
47
|
-
* @returns {Promise<Object[]>}
|
|
48
|
-
* @abstract
|
|
49
|
-
*/
|
|
50
|
-
async listMembershipsForOrganization(organizationUserId) {
|
|
51
|
-
throw new Error(
|
|
52
|
-
'Method listMembershipsForOrganization must be implemented by subclass'
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
module.exports = { OrganizationMembershipRepositoryInterface };
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
const {
|
|
2
|
-
OrganizationMembershipRepositoryPrisma,
|
|
3
|
-
} = require('./organization-membership-repository-prisma');
|
|
4
|
-
|
|
5
|
-
/** Organization membership persistence for MongoDB: ids are ObjectId strings, used as they are. */
|
|
6
|
-
class OrganizationMembershipRepositoryMongo extends OrganizationMembershipRepositoryPrisma {
|
|
7
|
-
/** @protected */
|
|
8
|
-
_toDbId(id) {
|
|
9
|
-
return id;
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
module.exports = { OrganizationMembershipRepositoryMongo };
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
const {
|
|
2
|
-
OrganizationMembershipRepositoryPrisma,
|
|
3
|
-
} = require('./organization-membership-repository-prisma');
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Organization membership persistence for PostgreSQL. The database keys are
|
|
7
|
-
* Int, the application's are strings, so ids are converted in and the record
|
|
8
|
-
* mapper stringifies them out.
|
|
9
|
-
*/
|
|
10
|
-
class OrganizationMembershipRepositoryPostgres extends OrganizationMembershipRepositoryPrisma {
|
|
11
|
-
/**
|
|
12
|
-
* Strict: digits only. `parseInt` would turn '12abc' into 12 and query
|
|
13
|
-
* the wrong person.
|
|
14
|
-
* @protected
|
|
15
|
-
*/
|
|
16
|
-
_toDbId(id) {
|
|
17
|
-
const text = typeof id === 'number' ? String(id) : id;
|
|
18
|
-
if (typeof text !== 'string' || !/^\d+$/.test(text)) {
|
|
19
|
-
throw new Error(`Invalid ID: ${id} cannot be converted to integer`);
|
|
20
|
-
}
|
|
21
|
-
const parsed = Number(text);
|
|
22
|
-
if (!Number.isSafeInteger(parsed)) {
|
|
23
|
-
throw new Error(`Invalid ID: ${id} cannot be converted to integer`);
|
|
24
|
-
}
|
|
25
|
-
return parsed;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
module.exports = { OrganizationMembershipRepositoryPostgres };
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
const { prisma } = require('../../database/prisma');
|
|
2
|
-
const { isUniqueViolation } = require('../../database/unique-violation');
|
|
3
|
-
const {
|
|
4
|
-
OrganizationMembershipRepositoryInterface,
|
|
5
|
-
} = require('./organization-membership-repository-interface');
|
|
6
|
-
const { toMembership } = require('./organization-membership-record');
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Organization membership persistence through the Prisma client. Both schemas
|
|
10
|
-
* declare the same model; they differ only in the id type, which the subclass
|
|
11
|
-
* supplies through `_toDbId` (MongoDB: ObjectId strings, PostgreSQL: Int).
|
|
12
|
-
*
|
|
13
|
-
* @abstract
|
|
14
|
-
*/
|
|
15
|
-
class OrganizationMembershipRepositoryPrisma extends OrganizationMembershipRepositoryInterface {
|
|
16
|
-
constructor(prismaClient = prisma) {
|
|
17
|
-
super();
|
|
18
|
-
this.prisma = prismaClient;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* @protected
|
|
23
|
-
* @abstract
|
|
24
|
-
*/
|
|
25
|
-
_toDbId(id) {
|
|
26
|
-
throw new Error('Method _toDbId must be implemented by subclass');
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async findMembership(individualUserId, organizationUserId) {
|
|
30
|
-
const row = await this._findRow(individualUserId, organizationUserId);
|
|
31
|
-
return row ? toMembership(row) : null;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async addMembership(individualUserId, organizationUserId, options = {}) {
|
|
35
|
-
const pair = this._pair(individualUserId, organizationUserId);
|
|
36
|
-
try {
|
|
37
|
-
const row = await this.prisma.organizationMembership.upsert({
|
|
38
|
-
where: { individualUserId_organizationUserId: pair },
|
|
39
|
-
// `undefined` lets the column default (MEMBER) apply.
|
|
40
|
-
create: { ...pair, orgRole: options.orgRole ?? undefined },
|
|
41
|
-
update: {},
|
|
42
|
-
});
|
|
43
|
-
return toMembership(row);
|
|
44
|
-
} catch (error) {
|
|
45
|
-
// Two first logins can both miss the row and both try to create it.
|
|
46
|
-
// The loser's insert hits the unique pair; the row is there now.
|
|
47
|
-
if (!isUniqueViolation(error)) throw error;
|
|
48
|
-
const row = await this._findRow(individualUserId, organizationUserId);
|
|
49
|
-
if (!row) throw error;
|
|
50
|
-
return toMembership(row);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async listMembershipsForOrganization(organizationUserId) {
|
|
55
|
-
const rows = await this.prisma.organizationMembership.findMany({
|
|
56
|
-
where: { organizationUserId: this._toDbId(organizationUserId) },
|
|
57
|
-
orderBy: { createdAt: 'asc' },
|
|
58
|
-
});
|
|
59
|
-
return rows.map(toMembership);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
_pair(individualUserId, organizationUserId) {
|
|
63
|
-
return {
|
|
64
|
-
individualUserId: this._toDbId(individualUserId),
|
|
65
|
-
organizationUserId: this._toDbId(organizationUserId),
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async _findRow(individualUserId, organizationUserId) {
|
|
70
|
-
return this.prisma.organizationMembership.findUnique({
|
|
71
|
-
where: {
|
|
72
|
-
individualUserId_organizationUserId: this._pair(
|
|
73
|
-
individualUserId,
|
|
74
|
-
organizationUserId
|
|
75
|
-
),
|
|
76
|
-
},
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
module.exports = { OrganizationMembershipRepositoryPrisma };
|