@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.
Files changed (161) hide show
  1. package/CHANGELOG.md +3 -0
  2. package/CLAUDE.md +0 -28
  3. package/README.md +0 -105
  4. package/application/commands/integration-commands.js +2 -2
  5. package/application/commands/scheduler-commands.js +7 -11
  6. package/application/commands/user-commands.js +5 -17
  7. package/core/Worker.js +7 -31
  8. package/core/create-handler.js +0 -9
  9. package/credential/use-cases/credential-operations-for-user.js +25 -110
  10. package/credential/use-cases/reauthorize-credential-for-user.js +8 -15
  11. package/database/config.js +24 -48
  12. package/database/documentdb-encryption-service.js +0 -1
  13. package/database/documentdb-utils.js +1 -6
  14. package/database/encryption/encryption-config.js +44 -13
  15. package/database/encryption/encryption-schema-registry.js +31 -88
  16. package/database/prisma.js +2 -4
  17. package/generated/prisma-mongodb/edge.js +4 -27
  18. package/generated/prisma-mongodb/index-browser.js +1 -24
  19. package/generated/prisma-mongodb/index.d.ts +48 -3228
  20. package/generated/prisma-mongodb/index.js +4 -27
  21. package/generated/prisma-mongodb/package.json +1 -1
  22. package/generated/prisma-mongodb/schema.prisma +0 -65
  23. package/generated/prisma-mongodb/wasm.js +4 -27
  24. package/generated/prisma-postgresql/edge.js +4 -27
  25. package/generated/prisma-postgresql/index-browser.js +1 -24
  26. package/generated/prisma-postgresql/index.d.ts +51 -3503
  27. package/generated/prisma-postgresql/index.js +4 -27
  28. package/generated/prisma-postgresql/package.json +1 -1
  29. package/generated/prisma-postgresql/schema.prisma +0 -61
  30. package/generated/prisma-postgresql/wasm.js +4 -27
  31. package/handlers/app-definition-loader.js +12 -110
  32. package/handlers/routers/auth.js +12 -40
  33. package/handlers/routers/health.js +2 -4
  34. package/handlers/routers/integration-defined-routers.js +92 -144
  35. package/handlers/routers/integration-webhook-routers.js +64 -102
  36. package/index.js +1 -64
  37. package/infrastructure/scheduler/index.js +6 -19
  38. package/infrastructure/scheduler/scheduler-service-factory.js +7 -55
  39. package/integrations/index.js +0 -5
  40. package/integrations/integration-router.js +73 -115
  41. package/integrations/repositories/integration-repository-documentdb.js +3 -35
  42. package/integrations/repositories/integration-repository-interface.js +1 -17
  43. package/integrations/repositories/integration-repository-mongo.js +76 -59
  44. package/integrations/repositories/integration-repository-postgres.js +76 -62
  45. package/integrations/tests/doubles/test-integration-repository.js +1 -12
  46. package/integrations/use-cases/create-integration.js +20 -55
  47. package/integrations/use-cases/get-integrations-for-user.js +40 -80
  48. package/integrations/use-cases/index.js +1 -7
  49. package/integrations/use-cases/integration-operations-for-user.js +24 -55
  50. package/integrations/utils/map-integration-dto.js +1 -44
  51. package/management-api/management-api-config.js +2 -71
  52. package/management-api/meta-router.js +2 -21
  53. package/management-api/openapi/components.js +3 -50
  54. package/management-api/openapi/v2-operations.js +5 -17
  55. package/management-api/route-registry.js +6 -123
  56. package/management-api/v2/dependencies.js +15 -40
  57. package/management-api/v2/dto.js +0 -20
  58. package/management-api/v2/handlers/authorization.js +0 -1
  59. package/management-api/v2/handlers/credentials.js +2 -6
  60. package/management-api/v2/handlers/entities.js +7 -10
  61. package/management-api/v2/handlers/integrations.js +3 -6
  62. package/management-api/v2/request.js +1 -32
  63. package/modules/index.js +0 -8
  64. package/modules/module-factory.js +0 -12
  65. package/modules/module.js +2 -53
  66. package/modules/proxy/proxy-errors.js +1 -12
  67. package/modules/proxy/proxy-path.js +3 -7
  68. package/modules/proxy/proxy-policy.js +5 -34
  69. package/modules/repositories/module-repository-documentdb.js +6 -51
  70. package/modules/repositories/module-repository-interface.js +2 -19
  71. package/modules/repositories/module-repository-mongo.js +74 -67
  72. package/modules/repositories/module-repository-postgres.js +74 -66
  73. package/modules/repositories/module-repository.js +1 -2
  74. package/modules/requester/proxy-request.js +1 -2
  75. package/modules/requester/requester.js +3 -8
  76. package/modules/use-cases/entity-operations-for-user.js +13 -50
  77. package/modules/use-cases/entity-types.js +7 -31
  78. package/modules/use-cases/execute-entity-proxy-request.js +15 -107
  79. package/modules/use-cases/get-authorization-requirements.js +2 -2
  80. package/modules/use-cases/get-entities-for-user.js +13 -26
  81. package/modules/use-cases/get-entity-module-for-user.js +7 -34
  82. package/modules/use-cases/get-entity-options-by-id.js +4 -10
  83. package/modules/use-cases/get-entity-options-by-type.js +1 -1
  84. package/modules/use-cases/get-module-instance-from-type.js +1 -1
  85. package/modules/use-cases/get-module.js +4 -10
  86. package/modules/use-cases/multi-step-authorization.js +13 -40
  87. package/modules/use-cases/process-authorization-callback.js +19 -53
  88. package/modules/use-cases/refresh-entity-options.js +4 -10
  89. package/modules/use-cases/test-module-auth.js +4 -10
  90. package/package.json +5 -6
  91. package/prisma-mongodb/schema.prisma +0 -65
  92. package/prisma-postgresql/schema.prisma +0 -61
  93. package/queues/index.js +0 -25
  94. package/types/index.d.ts +0 -1
  95. package/types/modules/index.d.ts +1 -157
  96. package/user/repositories/user-repository-documentdb.js +2 -27
  97. package/user/repositories/user-repository-interface.js +0 -18
  98. package/user/repositories/user-repository-mongo.js +0 -24
  99. package/user/repositories/user-repository-postgres.js +0 -18
  100. package/user/use-cases/authenticate-user.js +1 -7
  101. package/user/use-cases/get-user-from-adopter-jwt.js +124 -95
  102. package/user/use-cases/get-user-from-x-frigg-headers.js +80 -83
  103. package/user/user.js +1 -64
  104. package/database/organization-where.js +0 -18
  105. package/database/unique-violation.js +0 -25
  106. package/extensions/bootstrap-runner.js +0 -43
  107. package/extensions/extension-loader.js +0 -230
  108. package/extensions/index.js +0 -44
  109. package/extensions/initialize-app.js +0 -123
  110. package/extensions/prisma-composer.js +0 -270
  111. package/extensions/route-mounter.js +0 -103
  112. package/extensions/schema-composer.js +0 -188
  113. package/infrastructure/scheduler/netlify-scheduler-adapter.js +0 -216
  114. package/infrastructure/scheduler/scheduled-job-repository.js +0 -118
  115. package/integrations/repositories/integration-not-found.js +0 -14
  116. package/integrations/schedule-config.js +0 -80
  117. package/integrations/use-cases/dispatch-ongoing-syncs.js +0 -100
  118. package/integrations/use-cases/resolve-integration-ownership.js +0 -177
  119. package/modules/entity-scope.js +0 -15
  120. package/modules/proxy/proxy-bases.js +0 -169
  121. package/modules/repositories/entity-not-found.js +0 -18
  122. package/modules/scope-config.js +0 -192
  123. package/modules/scope-policy.js +0 -112
  124. package/modules/scope-resolution.js +0 -288
  125. package/modules/scope-visibility.js +0 -247
  126. package/modules/tests/doubles/ownership-matching.js +0 -18
  127. package/modules/use-cases/authorize-entity-for-user.js +0 -61
  128. package/modules/use-cases/create-entity-from-credential.js +0 -113
  129. package/modules/use-cases/manage-access.js +0 -21
  130. package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +0 -26
  131. package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +0 -25
  132. package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +0 -28
  133. package/providers/index.js +0 -23
  134. package/providers/resolve-provider.js +0 -93
  135. package/providers/runtime-detection.js +0 -124
  136. package/queues/providers/index.js +0 -11
  137. package/queues/providers/netlify-background-provider.js +0 -150
  138. package/queues/providers/qstash-queue-provider.js +0 -219
  139. package/queues/providers/sqs-queue-provider.js +0 -70
  140. package/queues/queue-client-interface.js +0 -55
  141. package/queues/queue-message-auth.js +0 -193
  142. package/queues/queue-provider-factory.js +0 -101
  143. package/queues/queue-provider.js +0 -65
  144. package/queues/sqs-queue-client.js +0 -81
  145. package/types/user/index.d.ts +0 -134
  146. package/user/adopter-jwt-config.js +0 -177
  147. package/user/adopter-jwt-errors.js +0 -59
  148. package/user/adopter-token-verifier.js +0 -122
  149. package/user/app-user-helpers.js +0 -97
  150. package/user/create-user-authentication.js +0 -64
  151. package/user/membership.js +0 -51
  152. package/user/repositories/organization-membership-record.js +0 -19
  153. package/user/repositories/organization-membership-repository-documentdb.js +0 -106
  154. package/user/repositories/organization-membership-repository-factory.js +0 -42
  155. package/user/repositories/organization-membership-repository-interface.js +0 -57
  156. package/user/repositories/organization-membership-repository-mongo.js +0 -13
  157. package/user/repositories/organization-membership-repository-postgres.js +0 -29
  158. package/user/repositories/organization-membership-repository-prisma.js +0 -81
  159. package/user/tests/doubles/adopter-jwt-fixtures.js +0 -220
  160. package/user/use-cases/ensure-organization-membership.js +0 -92
  161. 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
- ...envValues,
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` of `base`.
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
- * @param {string} [base='default'] - the named base the request targets; a
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, base = DEFAULT_BASE) {
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, base?: string, query?: Object, headers: Object, body?: string, requestBytes: number, bodyIsJson: boolean }}
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
- // A key ending in `[]` (`fields[]`) is sent once per value, as
110
- // Airtable and Fathom expect; every other array goes comma-separated.
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, ...(base !== undefined && { base }), query, headers, body, bodyIsJson, requestBytes };
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 entityNotFoundError(entityId);
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 entityNotFoundError(entityId);
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
- // Raw read to bypass the Prisma encryption extension. Drained:
284
- // more ids than the first cursor batch must all come back.
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
- organizationId: fromObjectId(doc?.organizationId) ?? null,
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 (provider fields included)
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, tenancy) {
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 entityNotFoundError(entityId);
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 this._mapEntity(entity, credential);
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) => (this._mapEntity(e, credentialMap.get(e.credentialId) || null)));
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) => (this._mapEntity(e, credentialMap.get(e.credentialId) || null)));
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) => (this._mapEntity(e, credentialMap.get(e.credentialId) || null)));
179
- }
180
-
181
- /**
182
- * Find entities matching any of the owner/organization pairs, in one query.
183
- * `organizationId: null` matches rows with no organization. The caller
184
- * (a use case) decides which pairs; this only runs them.
185
- *
186
- * @param {Array<{userId: string, organizationId: (string|null)}>} clauses
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 this._mapEntity(entity, credential);
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) => (this._mapEntity(e, credentialMap.get(e.credentialId) || null)));
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, tenancy = {}) {
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 this._mapEntity(entity, credentialObj);
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 this._mapEntity(entity, credentialObj);
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
  }