@friggframework/core 2.0.0--canary.707.df80bfd.0 → 2.0.0--canary.680.d670696.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/CHANGELOG.md +3 -0
- package/CLAUDE.md +0 -28
- package/README.md +0 -105
- package/application/commands/integration-commands.js +2 -2
- package/application/commands/scheduler-commands.js +7 -11
- package/application/commands/user-commands.js +5 -17
- package/core/Worker.js +7 -31
- package/core/create-handler.js +0 -9
- package/credential/use-cases/credential-operations-for-user.js +25 -110
- package/credential/use-cases/reauthorize-credential-for-user.js +8 -15
- package/database/config.js +24 -48
- package/database/documentdb-encryption-service.js +0 -1
- package/database/documentdb-utils.js +1 -6
- package/database/encryption/encryption-config.js +44 -13
- package/database/encryption/encryption-schema-registry.js +31 -88
- package/database/prisma.js +2 -4
- package/generated/prisma-mongodb/edge.js +4 -27
- package/generated/prisma-mongodb/index-browser.js +1 -24
- package/generated/prisma-mongodb/index.d.ts +48 -3228
- package/generated/prisma-mongodb/index.js +4 -27
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +0 -65
- package/generated/prisma-mongodb/wasm.js +4 -27
- package/generated/prisma-postgresql/edge.js +4 -27
- package/generated/prisma-postgresql/index-browser.js +1 -24
- package/generated/prisma-postgresql/index.d.ts +51 -3503
- package/generated/prisma-postgresql/index.js +4 -27
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +0 -61
- package/generated/prisma-postgresql/wasm.js +4 -27
- package/handlers/app-definition-loader.js +12 -110
- package/handlers/routers/auth.js +12 -40
- package/handlers/routers/health.js +2 -4
- package/handlers/routers/integration-defined-routers.js +92 -144
- package/handlers/routers/integration-webhook-routers.js +64 -102
- package/index.js +1 -64
- package/infrastructure/scheduler/index.js +6 -19
- package/infrastructure/scheduler/scheduler-service-factory.js +7 -55
- package/integrations/index.js +0 -5
- 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 -7
- package/integrations/use-cases/integration-operations-for-user.js +24 -55
- package/integrations/utils/map-integration-dto.js +1 -44
- package/management-api/management-api-config.js +2 -71
- package/management-api/meta-router.js +2 -21
- package/management-api/openapi/components.js +3 -50
- package/management-api/openapi/v2-operations.js +5 -17
- package/management-api/route-registry.js +6 -123
- 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/module-factory.js +0 -12
- package/modules/module.js +2 -53
- package/modules/proxy/proxy-errors.js +1 -12
- package/modules/proxy/proxy-path.js +3 -7
- package/modules/proxy/proxy-policy.js +5 -34
- package/modules/repositories/module-repository-documentdb.js +6 -51
- package/modules/repositories/module-repository-interface.js +2 -19
- package/modules/repositories/module-repository-mongo.js +74 -67
- package/modules/repositories/module-repository-postgres.js +74 -66
- package/modules/repositories/module-repository.js +1 -2
- package/modules/requester/proxy-request.js +1 -2
- package/modules/requester/requester.js +3 -8
- 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 +15 -107
- package/modules/use-cases/get-authorization-requirements.js +2 -2
- package/modules/use-cases/get-entities-for-user.js +13 -26
- package/modules/use-cases/get-entity-module-for-user.js +7 -34
- package/modules/use-cases/get-entity-options-by-id.js +4 -10
- package/modules/use-cases/get-entity-options-by-type.js +1 -1
- package/modules/use-cases/get-module-instance-from-type.js +1 -1
- package/modules/use-cases/get-module.js +4 -10
- package/modules/use-cases/multi-step-authorization.js +13 -40
- package/modules/use-cases/process-authorization-callback.js +19 -53
- package/modules/use-cases/refresh-entity-options.js +4 -10
- package/modules/use-cases/test-module-auth.js +4 -10
- package/package.json +5 -6
- package/prisma-mongodb/schema.prisma +0 -65
- package/prisma-postgresql/schema.prisma +0 -61
- package/queues/index.js +0 -25
- package/types/index.d.ts +0 -1
- package/types/modules/index.d.ts +1 -157
- 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/extensions/bootstrap-runner.js +0 -43
- package/extensions/extension-loader.js +0 -230
- package/extensions/index.js +0 -44
- package/extensions/initialize-app.js +0 -123
- package/extensions/prisma-composer.js +0 -270
- package/extensions/route-mounter.js +0 -103
- package/extensions/schema-composer.js +0 -188
- package/infrastructure/scheduler/netlify-scheduler-adapter.js +0 -216
- package/infrastructure/scheduler/scheduled-job-repository.js +0 -118
- package/integrations/repositories/integration-not-found.js +0 -14
- package/integrations/schedule-config.js +0 -80
- package/integrations/use-cases/dispatch-ongoing-syncs.js +0 -100
- package/integrations/use-cases/resolve-integration-ownership.js +0 -177
- package/modules/entity-scope.js +0 -15
- package/modules/proxy/proxy-bases.js +0 -169
- package/modules/repositories/entity-not-found.js +0 -18
- 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 -247
- 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/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +0 -28
- package/providers/index.js +0 -23
- package/providers/resolve-provider.js +0 -93
- package/providers/runtime-detection.js +0 -124
- package/queues/providers/index.js +0 -11
- package/queues/providers/netlify-background-provider.js +0 -150
- package/queues/providers/qstash-queue-provider.js +0 -219
- package/queues/providers/sqs-queue-provider.js +0 -70
- package/queues/queue-client-interface.js +0 -55
- package/queues/queue-message-auth.js +0 -193
- package/queues/queue-provider-factory.js +0 -101
- package/queues/queue-provider.js +0 -65
- package/queues/sqs-queue-client.js +0 -81
- 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/modules/module.js
CHANGED
|
@@ -21,17 +21,8 @@ class Module extends Delegate {
|
|
|
21
21
|
* @param {string} params.userId The user id
|
|
22
22
|
* @param {Object} params.entity The entity record from the database
|
|
23
23
|
* @param {string} [params.state] Optional OAuth state value forwarded to the API client (round-trips through the OAuth provider).
|
|
24
|
-
* @param {Object} [params.resolvedEnv] definition.env as Module.create() resolved it (may be undefined).
|
|
25
|
-
* @param {boolean} [params.envResolved=false] true when resolvedEnv carries the resolved env, even an undefined one; otherwise the static definition.env is used.
|
|
26
24
|
*/
|
|
27
|
-
constructor({
|
|
28
|
-
definition,
|
|
29
|
-
userId = null,
|
|
30
|
-
entity: entityObj = null,
|
|
31
|
-
state = null,
|
|
32
|
-
resolvedEnv,
|
|
33
|
-
envResolved = false,
|
|
34
|
-
}) {
|
|
25
|
+
constructor({ definition, userId = null, entity: entityObj = null, state = null }) {
|
|
35
26
|
super({ definition, userId, entity: entityObj });
|
|
36
27
|
|
|
37
28
|
this.validateDefinition(definition);
|
|
@@ -63,23 +54,8 @@ class Module extends Delegate {
|
|
|
63
54
|
|
|
64
55
|
Object.assign(this, this.definition.requiredAuthMethods);
|
|
65
56
|
|
|
66
|
-
// A pre-resolved env (Module.create()) supports definition.env as an
|
|
67
|
-
// async function, e.g. OAuth app credentials loaded from the database
|
|
68
|
-
// by the db-credentials extension; otherwise the static object.
|
|
69
|
-
// "Resolved" is an explicit flag, not a non-null value: a function env
|
|
70
|
-
// may legitimately resolve to undefined (no static env, no DB row),
|
|
71
|
-
// and falling back to definition.env would then read the function.
|
|
72
|
-
const envValues = envResolved ? resolvedEnv : this.definition.env;
|
|
73
|
-
if (typeof envValues === 'function') {
|
|
74
|
-
// Spreading a function yields no keys, so the API client would
|
|
75
|
-
// silently get no client_id / client_secret.
|
|
76
|
-
throw new TypeError(
|
|
77
|
-
`Module ${this.name}: definition.env is a function; build the module with await Module.create(...)`
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
57
|
const apiParams = {
|
|
82
|
-
...
|
|
58
|
+
...this.definition.env,
|
|
83
59
|
delegate: this,
|
|
84
60
|
logger: this.logger,
|
|
85
61
|
...(state ? { state } : {}),
|
|
@@ -91,33 +67,6 @@ class Module extends Delegate {
|
|
|
91
67
|
this.api = new this.apiClass(apiParams);
|
|
92
68
|
}
|
|
93
69
|
|
|
94
|
-
/**
|
|
95
|
-
* Resolves definition.env, supporting both a static object and a
|
|
96
|
-
* (possibly async) function.
|
|
97
|
-
*
|
|
98
|
-
* @param {Object} definition - Module definition
|
|
99
|
-
* @returns {Promise<Object|undefined>} Resolved env values
|
|
100
|
-
*/
|
|
101
|
-
static async resolveEnv(definition) {
|
|
102
|
-
if (typeof definition.env === 'function') {
|
|
103
|
-
return await definition.env();
|
|
104
|
-
}
|
|
105
|
-
return definition.env;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Async factory that resolves definition.env first, so a module whose
|
|
110
|
-
* env is a function gets its values. Prefer it over `new Module(...)`
|
|
111
|
-
* wherever the call site can await.
|
|
112
|
-
*
|
|
113
|
-
* @param {Object} params - same as the constructor, minus resolvedEnv
|
|
114
|
-
* @returns {Promise<Module>}
|
|
115
|
-
*/
|
|
116
|
-
static async create({ definition, userId = null, entity = null, state = null }) {
|
|
117
|
-
const resolvedEnv = await Module.resolveEnv(definition);
|
|
118
|
-
return new Module({ definition, userId, entity, state, resolvedEnv, envResolved: true });
|
|
119
|
-
}
|
|
120
|
-
|
|
121
70
|
getName() {
|
|
122
71
|
return this.name;
|
|
123
72
|
}
|
|
@@ -17,15 +17,4 @@ class ProxyRequestError extends Error {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
* A module definition or the app's proxy settings are wrong. Thrown at load,
|
|
22
|
-
* so the app stops starting instead of failing on the first proxied call.
|
|
23
|
-
*/
|
|
24
|
-
class ProxyConfigError extends Error {
|
|
25
|
-
constructor(message) {
|
|
26
|
-
super(message);
|
|
27
|
-
this.name = 'ProxyConfigError';
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
module.exports = { ProxyRequestError, ProxyConfigError };
|
|
20
|
+
module.exports = { ProxyRequestError };
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const { ProxyRequestError } = require('./proxy-errors');
|
|
2
|
-
const { DEFAULT_BASE, ruleBase } = require('./proxy-bases');
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* Path rules for the entity proxy (ADR-052 §4). The caller supplies a path
|
|
@@ -134,7 +133,7 @@ function joinProxyUrl(baseUrl, normalizedPath) {
|
|
|
134
133
|
}
|
|
135
134
|
|
|
136
135
|
/**
|
|
137
|
-
* Finds the allow-list rule that permits `method` on `path
|
|
136
|
+
* Finds the allow-list rule that permits `method` on `path`.
|
|
138
137
|
*
|
|
139
138
|
* Patterns use `:param` for exactly one segment and a trailing `/**` for a
|
|
140
139
|
* subtree (the prefix itself included). `method: '*'` matches any method.
|
|
@@ -142,17 +141,14 @@ function joinProxyUrl(baseUrl, normalizedPath) {
|
|
|
142
141
|
* @param {Array<{method: string, path: string}>} rules
|
|
143
142
|
* @param {string} method - upper case
|
|
144
143
|
* @param {string} path - normalised path
|
|
145
|
-
* @
|
|
146
|
-
* rule applies to its own `base` only (no `base` means `default`)
|
|
147
|
-
* @returns {{method: string, path: string, base?: string}|null}
|
|
144
|
+
* @returns {{method: string, path: string}|null}
|
|
148
145
|
*/
|
|
149
|
-
function matchAllowRule(rules, method, path
|
|
146
|
+
function matchAllowRule(rules, method, path) {
|
|
150
147
|
const requestSegments = path.split('/').slice(1);
|
|
151
148
|
for (const rule of rules || []) {
|
|
152
149
|
if (!rule || typeof rule.path !== 'string' || typeof rule.method !== 'string') {
|
|
153
150
|
continue;
|
|
154
151
|
}
|
|
155
|
-
if (ruleBase(rule) !== base) continue;
|
|
156
152
|
const ruleMethod = rule.method.toUpperCase();
|
|
157
153
|
if (ruleMethod !== '*' && ruleMethod !== method) continue;
|
|
158
154
|
if (patternMatches(rule.path, requestSegments)) return rule;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const { ProxyRequestError } = require('./proxy-errors');
|
|
2
|
-
const { isValidBaseName } = require('./proxy-bases');
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* What the entity proxy accepts and returns (ADR-052 §5-§7). Pure functions:
|
|
@@ -53,13 +52,6 @@ const REMOVED_RESPONSE_HEADERS = [
|
|
|
53
52
|
const invalid = (message, details) =>
|
|
54
53
|
new ProxyRequestError('INVALID_PROXY_REQUEST', message, details);
|
|
55
54
|
|
|
56
|
-
/**
|
|
57
|
-
* A query key ending in `[]` is sent as one `key=value` pair per array
|
|
58
|
-
* element; the requester applies the same rule (see
|
|
59
|
-
* `_rawRequest` in requester.js).
|
|
60
|
-
*/
|
|
61
|
-
const isRepeatedQueryKey = (key) => key.endsWith('[]');
|
|
62
|
-
|
|
63
55
|
const isPlainObject = (value) =>
|
|
64
56
|
value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
65
57
|
|
|
@@ -69,7 +61,7 @@ const isPlainObject = (value) =>
|
|
|
69
61
|
*
|
|
70
62
|
* @param {unknown} request
|
|
71
63
|
* @param {{ maxRequestBytes: number }} limits
|
|
72
|
-
* @returns {{ method: string, path: string,
|
|
64
|
+
* @returns {{ method: string, path: string, query?: Object, headers: Object, body?: string, requestBytes: number, bodyIsJson: boolean }}
|
|
73
65
|
*/
|
|
74
66
|
function validateProxyRequest(request, { maxRequestBytes }) {
|
|
75
67
|
if (!isPlainObject(request)) throw invalid('The proxy request must be a JSON object');
|
|
@@ -81,19 +73,6 @@ function validateProxyRequest(request, { maxRequestBytes }) {
|
|
|
81
73
|
throw invalid('path is required', { field: 'path' });
|
|
82
74
|
}
|
|
83
75
|
|
|
84
|
-
// `base` is a name, never a URL or host; anything else is refused before
|
|
85
|
-
// a module is looked up. Omitted means `default` (the caller applies it).
|
|
86
|
-
let base;
|
|
87
|
-
if (request.base !== undefined && request.base !== null) {
|
|
88
|
-
if (typeof request.base !== 'string' || !isValidBaseName(request.base)) {
|
|
89
|
-
throw invalid(
|
|
90
|
-
'base must be the name of one of the module\'s base URLs (lower case letters, digits and -), not a URL or host',
|
|
91
|
-
{ field: 'base' }
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
base = request.base;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
76
|
let query;
|
|
98
77
|
if (request.query !== undefined && request.query !== null) {
|
|
99
78
|
if (!isPlainObject(request.query)) throw invalid('query must be an object', { field: 'query' });
|
|
@@ -106,13 +85,8 @@ function validateProxyRequest(request, { maxRequestBytes }) {
|
|
|
106
85
|
field: `query.${key}`,
|
|
107
86
|
});
|
|
108
87
|
}
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
if (list) {
|
|
112
|
-
query[key] = isRepeatedQueryKey(key) ? [...value] : value.join(',');
|
|
113
|
-
} else {
|
|
114
|
-
query[key] = value;
|
|
115
|
-
}
|
|
88
|
+
// The requester sends one value per key; arrays go comma-separated.
|
|
89
|
+
query[key] = list ? value.join(',') : value;
|
|
116
90
|
}
|
|
117
91
|
}
|
|
118
92
|
|
|
@@ -147,19 +121,16 @@ function validateProxyRequest(request, { maxRequestBytes }) {
|
|
|
147
121
|
`The proxied request body is larger than ${maxRequestBytes} bytes`
|
|
148
122
|
);
|
|
149
123
|
}
|
|
150
|
-
return { method, path: request.path,
|
|
124
|
+
return { method, path: request.path, query, headers, body, bodyIsJson, requestBytes };
|
|
151
125
|
}
|
|
152
126
|
|
|
153
127
|
const isWildcardRule = (rule) => rule?.method === '*' || rule?.path === '/**';
|
|
154
128
|
|
|
155
|
-
// A rule's `base` is checked against the module's declared bases at load, by
|
|
156
|
-
// assertProxyConfig (proxy-bases.js); here only its shape is.
|
|
157
129
|
const isValidRule = (rule) =>
|
|
158
130
|
isPlainObject(rule) &&
|
|
159
131
|
typeof rule.method === 'string' &&
|
|
160
132
|
typeof rule.path === 'string' &&
|
|
161
|
-
rule.path.startsWith('/')
|
|
162
|
-
(rule.base === undefined || isValidBaseName(rule.base));
|
|
133
|
+
rule.path.startsWith('/');
|
|
163
134
|
|
|
164
135
|
/**
|
|
165
136
|
* The allow-list in force for a module: the app definition's list for it when
|
|
@@ -3,14 +3,12 @@ const {
|
|
|
3
3
|
toObjectId,
|
|
4
4
|
fromObjectId,
|
|
5
5
|
findMany,
|
|
6
|
-
findManyDrained,
|
|
7
6
|
findOne,
|
|
8
7
|
insertOne,
|
|
9
8
|
updateOne,
|
|
10
9
|
deleteOne,
|
|
11
10
|
} = require('../../database/documentdb-utils');
|
|
12
11
|
const { ModuleRepositoryInterface } = require('./module-repository-interface');
|
|
13
|
-
const { entityNotFoundError } = require('./entity-not-found');
|
|
14
12
|
const { DocumentDBEncryptionService } = require('../../database/documentdb-encryption-service');
|
|
15
13
|
|
|
16
14
|
/**
|
|
@@ -35,11 +33,11 @@ class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
|
|
|
35
33
|
async findEntityById(entityId) {
|
|
36
34
|
const objectId = toObjectId(entityId);
|
|
37
35
|
if (!objectId) {
|
|
38
|
-
throw
|
|
36
|
+
throw new Error(`Entity ${entityId} not found`);
|
|
39
37
|
}
|
|
40
38
|
const doc = await findOne(this.prisma, 'Entity', { _id: objectId });
|
|
41
39
|
if (!doc) {
|
|
42
|
-
throw
|
|
40
|
+
throw new Error(`Entity ${entityId} not found`);
|
|
43
41
|
}
|
|
44
42
|
const credential = await this._fetchCredential(doc.credentialId);
|
|
45
43
|
return this._mapEntity(doc, credential);
|
|
@@ -78,29 +76,6 @@ class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
|
|
|
78
76
|
return docs.map((doc) => this._mapEntity(doc, credentialMap.get(fromObjectId(doc.credentialId)) || null));
|
|
79
77
|
}
|
|
80
78
|
|
|
81
|
-
/**
|
|
82
|
-
* Find entities matching any of the owner/organization pairs, in one query.
|
|
83
|
-
* `organizationId: null` matches rows with no organization (a raw MongoDB
|
|
84
|
-
* `null` also matches a missing field). The caller (a use case) decides
|
|
85
|
-
* which pairs; this only runs them.
|
|
86
|
-
*
|
|
87
|
-
* @param {Array<{userId: string, organizationId: (string|null)}>} clauses
|
|
88
|
-
* @returns {Promise<Array>}
|
|
89
|
-
*/
|
|
90
|
-
async findEntitiesByOwnership(clauses) {
|
|
91
|
-
if (!Array.isArray(clauses) || clauses.length === 0) return [];
|
|
92
|
-
// Drained: a list can hold more than the first cursor batch (~101).
|
|
93
|
-
const docs = await findManyDrained(this.prisma, 'Entity', {
|
|
94
|
-
$or: clauses.map(({ userId, organizationId }) => ({
|
|
95
|
-
userId: toObjectId(userId),
|
|
96
|
-
organizationId:
|
|
97
|
-
organizationId === null ? null : toObjectId(organizationId),
|
|
98
|
-
})),
|
|
99
|
-
});
|
|
100
|
-
const credentialMap = await this._fetchCredentialsBulk(docs.map((doc) => doc.credentialId));
|
|
101
|
-
return docs.map((doc) => this._mapEntity(doc, credentialMap.get(fromObjectId(doc.credentialId)) || null));
|
|
102
|
-
}
|
|
103
|
-
|
|
104
79
|
async unsetCredential(entityId) {
|
|
105
80
|
const objectId = toObjectId(entityId);
|
|
106
81
|
if (!objectId) return false;
|
|
@@ -140,14 +115,7 @@ class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
|
|
|
140
115
|
);
|
|
141
116
|
}
|
|
142
117
|
|
|
143
|
-
|
|
144
|
-
* @param {Object} entityData
|
|
145
|
-
* @param {{organizationId?: (string|null)}} [tenancy] - the organization the entity
|
|
146
|
-
* belongs to (ADR-056 §3.6). A separate argument, not a field of `entityData`,
|
|
147
|
-
* because `entityData` carries provider fields and one named `organizationId`
|
|
148
|
-
* must not be taken for ours.
|
|
149
|
-
*/
|
|
150
|
-
async createEntity(entityData, tenancy = {}) {
|
|
118
|
+
async createEntity(entityData) {
|
|
151
119
|
const {
|
|
152
120
|
user,
|
|
153
121
|
userId,
|
|
@@ -165,7 +133,6 @@ class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
|
|
|
165
133
|
name: name ?? null,
|
|
166
134
|
moduleName: moduleName ?? null,
|
|
167
135
|
externalId: externalId ?? null,
|
|
168
|
-
organizationId: toObjectId(tenancy.organizationId) || null,
|
|
169
136
|
data: dynamicData,
|
|
170
137
|
};
|
|
171
138
|
const insertedId = await insertOne(this.prisma, 'Entity', document);
|
|
@@ -280,9 +247,8 @@ class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
|
|
|
280
247
|
const objectIds = ids.map(id => toObjectId(id)).filter(Boolean);
|
|
281
248
|
if (objectIds.length === 0) return new Map();
|
|
282
249
|
|
|
283
|
-
//
|
|
284
|
-
|
|
285
|
-
const rawCredentials = await findManyDrained(this.prisma, 'Credential', {
|
|
250
|
+
// Use raw findMany to bypass Prisma encryption extension
|
|
251
|
+
const rawCredentials = await findMany(this.prisma, 'Credential', {
|
|
286
252
|
_id: { $in: objectIds }
|
|
287
253
|
});
|
|
288
254
|
|
|
@@ -363,30 +329,19 @@ class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
|
|
|
363
329
|
if (filter.name) query.name = filter.name;
|
|
364
330
|
if (filter.moduleName) query.moduleName = filter.moduleName;
|
|
365
331
|
if (filter.externalId) query.externalId = filter.externalId;
|
|
366
|
-
// `null` means "no organization"; absent means "any". A raw MongoDB
|
|
367
|
-
// `{ field: null }` matches both a null value and a missing field.
|
|
368
|
-
if (filter.organizationId !== undefined) {
|
|
369
|
-
query.organizationId =
|
|
370
|
-
filter.organizationId === null
|
|
371
|
-
? null
|
|
372
|
-
: toObjectId(filter.organizationId);
|
|
373
|
-
}
|
|
374
332
|
return query;
|
|
375
333
|
}
|
|
376
334
|
|
|
377
335
|
_mapEntity(doc, credential) {
|
|
378
336
|
const dynamicData = doc?.data || {};
|
|
379
337
|
return {
|
|
380
|
-
// First, so no stored provider field can replace a column below
|
|
381
|
-
// (identity, owner or organization decide who sees the entity).
|
|
382
|
-
...dynamicData,
|
|
383
338
|
id: fromObjectId(doc?._id),
|
|
384
339
|
credential,
|
|
385
340
|
userId: fromObjectId(doc?.userId),
|
|
386
341
|
name: doc?.name ?? null,
|
|
387
342
|
externalId: doc?.externalId ?? null,
|
|
388
343
|
moduleName: doc?.moduleName ?? null,
|
|
389
|
-
|
|
344
|
+
...dynamicData,
|
|
390
345
|
};
|
|
391
346
|
}
|
|
392
347
|
}
|
|
@@ -67,21 +67,6 @@ class ModuleRepositoryInterface {
|
|
|
67
67
|
);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
/**
|
|
71
|
-
* Find entities matching any of the owner/organization pairs, in one query
|
|
72
|
-
* (ADR-056 §3.6). `organizationId: null` matches rows with no organization.
|
|
73
|
-
* Which pairs is decided by the caller (modules/entity-scope.js), not here.
|
|
74
|
-
*
|
|
75
|
-
* @param {Array<{userId: string|number, organizationId: (string|number|null)}>} clauses
|
|
76
|
-
* @returns {Promise<Array>} Array of entity objects
|
|
77
|
-
* @abstract
|
|
78
|
-
*/
|
|
79
|
-
async findEntitiesByOwnership(clauses) {
|
|
80
|
-
throw new Error(
|
|
81
|
-
'Method findEntitiesByOwnership must be implemented by subclass'
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
70
|
/**
|
|
86
71
|
* Unset credential from entity
|
|
87
72
|
*
|
|
@@ -125,13 +110,11 @@ class ModuleRepositoryInterface {
|
|
|
125
110
|
/**
|
|
126
111
|
* Create a new entity
|
|
127
112
|
*
|
|
128
|
-
* @param {Object} entityData - Entity data
|
|
129
|
-
* @param {{organizationId?: (string|number|null)}} [tenancy] - the organization
|
|
130
|
-
* the entity belongs to (ADR-056 §3.6), kept apart from `entityData`
|
|
113
|
+
* @param {Object} entityData - Entity data
|
|
131
114
|
* @returns {Promise<Object>} Created entity object
|
|
132
115
|
* @abstract
|
|
133
116
|
*/
|
|
134
|
-
async createEntity(entityData
|
|
117
|
+
async createEntity(entityData) {
|
|
135
118
|
throw new Error('Method createEntity must be implemented by subclass');
|
|
136
119
|
}
|
|
137
120
|
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
const { prisma } = require('../../database/prisma');
|
|
2
|
-
const { organizationWhere } = require('../../database/organization-where');
|
|
3
2
|
const { ModuleRepositoryInterface } = require('./module-repository-interface');
|
|
4
|
-
const { entityNotFoundError } = require('./entity-not-found');
|
|
5
3
|
|
|
6
4
|
/**
|
|
7
5
|
* MongoDB Module Repository Adapter
|
|
@@ -78,26 +76,6 @@ class ModuleRepositoryMongo extends ModuleRepositoryInterface {
|
|
|
78
76
|
return credentialMap;
|
|
79
77
|
}
|
|
80
78
|
|
|
81
|
-
/**
|
|
82
|
-
* The one place a stored entity row becomes the entity the application sees.
|
|
83
|
-
* The provider's `data` is spread FIRST and every column is pinned after it:
|
|
84
|
-
* a stored `id`, `userId` or `organizationId` in `data` must not decide who
|
|
85
|
-
* owns the entity or who sees it.
|
|
86
|
-
* @private
|
|
87
|
-
*/
|
|
88
|
-
_mapEntity(row, credential) {
|
|
89
|
-
return {
|
|
90
|
-
...(row.data || {}),
|
|
91
|
-
id: row.id,
|
|
92
|
-
credential,
|
|
93
|
-
userId: row.userId,
|
|
94
|
-
name: row.name,
|
|
95
|
-
externalId: row.externalId,
|
|
96
|
-
moduleName: row.moduleName,
|
|
97
|
-
organizationId: row.organizationId ?? null,
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
79
|
/**
|
|
102
80
|
* Find entity by ID with credential
|
|
103
81
|
* Replaces: Entity.findById(entityId).populate('credential')
|
|
@@ -112,12 +90,20 @@ class ModuleRepositoryMongo extends ModuleRepositoryInterface {
|
|
|
112
90
|
});
|
|
113
91
|
|
|
114
92
|
if (!entity) {
|
|
115
|
-
throw
|
|
93
|
+
throw new Error(`Entity ${entityId} not found`);
|
|
116
94
|
}
|
|
117
95
|
|
|
118
96
|
const credential = await this._fetchCredential(entity.credentialId);
|
|
119
97
|
|
|
120
|
-
return
|
|
98
|
+
return {
|
|
99
|
+
id: entity.id,
|
|
100
|
+
credential,
|
|
101
|
+
userId: entity.userId,
|
|
102
|
+
name: entity.name,
|
|
103
|
+
externalId: entity.externalId,
|
|
104
|
+
moduleName: entity.moduleName,
|
|
105
|
+
...(entity.data || {}),
|
|
106
|
+
};
|
|
121
107
|
}
|
|
122
108
|
|
|
123
109
|
/**
|
|
@@ -135,7 +121,15 @@ class ModuleRepositoryMongo extends ModuleRepositoryInterface {
|
|
|
135
121
|
const credentialIds = entities.map(e => e.credentialId).filter(Boolean);
|
|
136
122
|
const credentialMap = await this._fetchCredentialsBulk(credentialIds);
|
|
137
123
|
|
|
138
|
-
return entities.map((e) => (
|
|
124
|
+
return entities.map((e) => ({
|
|
125
|
+
id: e.id,
|
|
126
|
+
credential: credentialMap.get(e.credentialId) || null,
|
|
127
|
+
userId: e.userId,
|
|
128
|
+
name: e.name,
|
|
129
|
+
externalId: e.externalId,
|
|
130
|
+
moduleName: e.moduleName,
|
|
131
|
+
...(e.data || {}),
|
|
132
|
+
}));
|
|
139
133
|
}
|
|
140
134
|
|
|
141
135
|
/**
|
|
@@ -153,7 +147,15 @@ class ModuleRepositoryMongo extends ModuleRepositoryInterface {
|
|
|
153
147
|
const credentialIds = entities.map(e => e.credentialId).filter(Boolean);
|
|
154
148
|
const credentialMap = await this._fetchCredentialsBulk(credentialIds);
|
|
155
149
|
|
|
156
|
-
return entities.map((e) => (
|
|
150
|
+
return entities.map((e) => ({
|
|
151
|
+
id: e.id,
|
|
152
|
+
credential: credentialMap.get(e.credentialId) || null,
|
|
153
|
+
userId: e.userId,
|
|
154
|
+
name: e.name,
|
|
155
|
+
externalId: e.externalId,
|
|
156
|
+
moduleName: e.moduleName,
|
|
157
|
+
...(e.data || {}),
|
|
158
|
+
}));
|
|
157
159
|
}
|
|
158
160
|
|
|
159
161
|
/**
|
|
@@ -175,32 +177,15 @@ class ModuleRepositoryMongo extends ModuleRepositoryInterface {
|
|
|
175
177
|
const credentialIds = entities.map(e => e.credentialId).filter(Boolean);
|
|
176
178
|
const credentialMap = await this._fetchCredentialsBulk(credentialIds);
|
|
177
179
|
|
|
178
|
-
return entities.map((e) => (
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
* @returns {Promise<Array>} Array of entity objects with string IDs
|
|
188
|
-
*/
|
|
189
|
-
async findEntitiesByOwnership(clauses) {
|
|
190
|
-
if (!Array.isArray(clauses) || clauses.length === 0) return [];
|
|
191
|
-
const entities = await this.prisma.entity.findMany({
|
|
192
|
-
where: {
|
|
193
|
-
OR: clauses.map(({ userId, organizationId }) => ({
|
|
194
|
-
userId,
|
|
195
|
-
...organizationWhere(organizationId),
|
|
196
|
-
})),
|
|
197
|
-
},
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
const credentialIds = entities.map(e => e.credentialId).filter(Boolean);
|
|
201
|
-
const credentialMap = await this._fetchCredentialsBulk(credentialIds);
|
|
202
|
-
|
|
203
|
-
return entities.map((e) => (this._mapEntity(e, credentialMap.get(e.credentialId) || null)));
|
|
180
|
+
return entities.map((e) => ({
|
|
181
|
+
id: e.id,
|
|
182
|
+
credential: credentialMap.get(e.credentialId) || null,
|
|
183
|
+
userId: e.userId,
|
|
184
|
+
name: e.name,
|
|
185
|
+
externalId: e.externalId,
|
|
186
|
+
moduleName: e.moduleName,
|
|
187
|
+
...(e.data || {}),
|
|
188
|
+
}));
|
|
204
189
|
}
|
|
205
190
|
|
|
206
191
|
/**
|
|
@@ -238,7 +223,15 @@ class ModuleRepositoryMongo extends ModuleRepositoryInterface {
|
|
|
238
223
|
|
|
239
224
|
const credential = await this._fetchCredential(entity.credentialId);
|
|
240
225
|
|
|
241
|
-
return
|
|
226
|
+
return {
|
|
227
|
+
id: entity.id,
|
|
228
|
+
credential,
|
|
229
|
+
userId: entity.userId,
|
|
230
|
+
name: entity.name,
|
|
231
|
+
externalId: entity.externalId,
|
|
232
|
+
moduleName: entity.moduleName,
|
|
233
|
+
...(entity.data || {}),
|
|
234
|
+
};
|
|
242
235
|
}
|
|
243
236
|
|
|
244
237
|
/**
|
|
@@ -258,7 +251,15 @@ class ModuleRepositoryMongo extends ModuleRepositoryInterface {
|
|
|
258
251
|
.filter(Boolean);
|
|
259
252
|
const credentialMap = await this._fetchCredentialsBulk(credentialIds);
|
|
260
253
|
|
|
261
|
-
return entities.map((e) => (
|
|
254
|
+
return entities.map((e) => ({
|
|
255
|
+
id: e.id,
|
|
256
|
+
credential: credentialMap.get(e.credentialId) || null,
|
|
257
|
+
userId: e.userId,
|
|
258
|
+
name: e.name,
|
|
259
|
+
externalId: e.externalId,
|
|
260
|
+
moduleName: e.moduleName,
|
|
261
|
+
...(e.data || {}),
|
|
262
|
+
}));
|
|
262
263
|
}
|
|
263
264
|
|
|
264
265
|
/**
|
|
@@ -266,13 +267,9 @@ class ModuleRepositoryMongo extends ModuleRepositoryInterface {
|
|
|
266
267
|
* Replaces: Entity.create(entityData)
|
|
267
268
|
*
|
|
268
269
|
* @param {Object} entityData - Entity data
|
|
269
|
-
* @param {{organizationId?: (string|null)}} [tenancy] - the organization the entity
|
|
270
|
-
* belongs to (ADR-056 §3.6). A separate argument, not a field of `entityData`,
|
|
271
|
-
* because `entityData` carries provider fields and one named `organizationId`
|
|
272
|
-
* must not be taken for ours.
|
|
273
270
|
* @returns {Promise<Object>} Created entity object with string IDs
|
|
274
271
|
*/
|
|
275
|
-
async createEntity(entityData
|
|
272
|
+
async createEntity(entityData) {
|
|
276
273
|
const {
|
|
277
274
|
user,
|
|
278
275
|
userId,
|
|
@@ -290,7 +287,6 @@ class ModuleRepositoryMongo extends ModuleRepositoryInterface {
|
|
|
290
287
|
name,
|
|
291
288
|
moduleName,
|
|
292
289
|
externalId,
|
|
293
|
-
organizationId: tenancy.organizationId ?? null,
|
|
294
290
|
data: dynamicData,
|
|
295
291
|
};
|
|
296
292
|
|
|
@@ -300,7 +296,15 @@ class ModuleRepositoryMongo extends ModuleRepositoryInterface {
|
|
|
300
296
|
|
|
301
297
|
const credentialObj = await this._fetchCredential(entity.credentialId);
|
|
302
298
|
|
|
303
|
-
return
|
|
299
|
+
return {
|
|
300
|
+
id: entity.id,
|
|
301
|
+
credential: credentialObj,
|
|
302
|
+
userId: entity.userId,
|
|
303
|
+
name: entity.name,
|
|
304
|
+
externalId: entity.externalId,
|
|
305
|
+
moduleName: entity.moduleName,
|
|
306
|
+
...(entity.data || {}),
|
|
307
|
+
};
|
|
304
308
|
}
|
|
305
309
|
|
|
306
310
|
/**
|
|
@@ -354,7 +358,15 @@ class ModuleRepositoryMongo extends ModuleRepositoryInterface {
|
|
|
354
358
|
|
|
355
359
|
const credentialObj = await this._fetchCredential(entity.credentialId);
|
|
356
360
|
|
|
357
|
-
return
|
|
361
|
+
return {
|
|
362
|
+
id: entity.id,
|
|
363
|
+
credential: credentialObj,
|
|
364
|
+
userId: entity.userId,
|
|
365
|
+
name: entity.name,
|
|
366
|
+
externalId: entity.externalId,
|
|
367
|
+
moduleName: entity.moduleName,
|
|
368
|
+
...(entity.data || {}),
|
|
369
|
+
};
|
|
358
370
|
} catch (error) {
|
|
359
371
|
if (error.code === 'P2025') {
|
|
360
372
|
return null;
|
|
@@ -417,11 +429,6 @@ class ModuleRepositoryMongo extends ModuleRepositoryInterface {
|
|
|
417
429
|
if (filter.moduleName) where.moduleName = filter.moduleName;
|
|
418
430
|
if (filter.externalId) where.externalId = this._toString(filter.externalId);
|
|
419
431
|
|
|
420
|
-
// `null` means "no organization"; absent means "any".
|
|
421
|
-
if (filter.organizationId !== undefined) {
|
|
422
|
-
Object.assign(where, organizationWhere(filter.organizationId));
|
|
423
|
-
}
|
|
424
|
-
|
|
425
432
|
return where;
|
|
426
433
|
}
|
|
427
434
|
}
|