@friggframework/core 2.0.0--canary.681.2df9ef0.0 → 2.0.0--canary.682.cbca9e2.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/README.md +36 -0
- package/application/commands/integration-commands.js +20 -10
- package/application/index.js +6 -2
- package/database/encryption/encryption-schema-registry.js +7 -0
- package/database/organization-where.js +18 -0
- package/generated/prisma-mongodb/edge.js +5 -3
- package/generated/prisma-mongodb/index-browser.js +2 -0
- package/generated/prisma-mongodb/index.d.ts +115 -2
- package/generated/prisma-mongodb/index.js +5 -3
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +15 -0
- package/generated/prisma-mongodb/wasm.js +5 -3
- package/generated/prisma-postgresql/edge.js +5 -3
- package/generated/prisma-postgresql/index-browser.js +2 -0
- package/generated/prisma-postgresql/index.d.ts +131 -2
- package/generated/prisma-postgresql/index.js +5 -3
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +15 -0
- package/generated/prisma-postgresql/wasm.js +5 -3
- package/handlers/app-definition-loader.js +25 -8
- package/handlers/backend-utils.js +31 -25
- package/handlers/routers/health.js +1 -4
- package/handlers/workers/integration-defined-workers.js +5 -2
- package/index.js +10 -2
- package/integrations/index.js +0 -4
- package/integrations/integration-router.js +99 -52
- package/integrations/repositories/integration-not-found.js +14 -0
- package/integrations/repositories/integration-repository-documentdb.js +34 -3
- package/integrations/repositories/integration-repository-interface.js +17 -1
- package/integrations/repositories/integration-repository-mongo.js +59 -76
- package/integrations/repositories/integration-repository-postgres.js +62 -76
- package/integrations/tests/doubles/test-integration-repository.js +12 -1
- package/integrations/use-cases/create-integration.js +55 -20
- package/integrations/use-cases/get-integrations-for-user.js +80 -40
- package/integrations/use-cases/index.js +5 -1
- package/integrations/use-cases/integration-operations-for-user.js +40 -19
- package/integrations/use-cases/resolve-integration-ownership.js +177 -0
- package/integrations/utils/map-integration-dto.js +1 -15
- package/management-api/management-api-config.js +3 -0
- package/management-api/openapi/components.js +46 -1
- package/management-api/openapi/v2-operations.js +14 -1
- package/management-api/v2/dependencies.js +56 -14
- package/management-api/v2/dto.js +17 -0
- package/management-api/v2/handlers/authorization.js +1 -0
- package/management-api/v2/handlers/entities.js +10 -7
- package/management-api/v2/handlers/integrations.js +6 -3
- package/management-api/v2/request.js +32 -1
- package/modules/entity-scope.js +15 -0
- package/modules/entity-type-registry.js +311 -0
- package/modules/index.js +14 -0
- package/modules/repositories/module-repository-documentdb.js +41 -1
- package/modules/repositories/module-repository-interface.js +19 -2
- package/modules/repositories/module-repository-mongo.js +64 -73
- package/modules/repositories/module-repository-postgres.js +63 -73
- package/modules/scope-config.js +155 -0
- package/modules/scope-policy.js +80 -0
- package/modules/scope-resolution.js +277 -0
- package/modules/scope-visibility.js +165 -0
- package/modules/tests/doubles/ownership-matching.js +18 -0
- package/modules/use-cases/authorize-entity-for-user.js +59 -0
- package/modules/use-cases/entity-operations-for-user.js +42 -10
- package/modules/use-cases/entity-types.js +62 -14
- package/modules/use-cases/execute-entity-proxy-request.js +15 -0
- package/modules/use-cases/get-entities-for-user.js +16 -5
- package/modules/use-cases/get-entity-module-for-user.js +23 -3
- package/modules/use-cases/get-entity-options-by-id.js +5 -2
- package/modules/use-cases/get-module.js +5 -2
- package/modules/use-cases/multi-step-authorization.js +36 -12
- package/modules/use-cases/process-authorization-callback.js +52 -18
- package/modules/use-cases/refresh-entity-options.js +5 -2
- package/modules/use-cases/test-module-auth.js +5 -2
- package/package.json +5 -5
- package/prisma-mongodb/schema.prisma +15 -0
- package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +25 -0
- package/prisma-postgresql/schema.prisma +15 -0
- package/types/index.d.ts +1 -0
- package/types/modules/index.d.ts +145 -0
|
@@ -2,7 +2,16 @@
|
|
|
2
2
|
* Wires the repositories and use cases behind Management API v2. Kept apart
|
|
3
3
|
* from the router so tests can hand the router doubles instead.
|
|
4
4
|
*/
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* @param {Object} params
|
|
7
|
+
* @param {Object} params.appDefinition - the loaded app definition. Its
|
|
8
|
+
* `entityTypeRegistry` (built once by loadAppDefinition) is used.
|
|
9
|
+
* @param {Object} params.config
|
|
10
|
+
* @param {Map} [params.entityTypeRegistry] - overrides the registry on the
|
|
11
|
+
* app definition. When neither exists (a raw definition in a test), it is
|
|
12
|
+
* built here from the definition.
|
|
13
|
+
*/
|
|
14
|
+
function buildV2Dependencies({ appDefinition, config, entityTypeRegistry }) {
|
|
6
15
|
const {
|
|
7
16
|
createIntegrationRepository,
|
|
8
17
|
} = require('../../integrations/repositories/integration-repository-factory');
|
|
@@ -17,8 +26,16 @@ function buildV2Dependencies({ appDefinition, config }) {
|
|
|
17
26
|
} = require('../../user/repositories/user-repository-factory');
|
|
18
27
|
const { ModuleFactory } = require('../../modules/module-factory');
|
|
19
28
|
const {
|
|
20
|
-
|
|
21
|
-
|
|
29
|
+
getEntityTypeRegistry,
|
|
30
|
+
getEntityTypeDefinitions,
|
|
31
|
+
} = require('../../modules/entity-type-registry');
|
|
32
|
+
const { createScopePolicy } = require('../../modules/scope-policy');
|
|
33
|
+
const {
|
|
34
|
+
AuthorizeEntityForUser,
|
|
35
|
+
} = require('../../modules/use-cases/authorize-entity-for-user');
|
|
36
|
+
const {
|
|
37
|
+
ResolveIntegrationOwnership,
|
|
38
|
+
} = require('../../integrations/use-cases/resolve-integration-ownership');
|
|
22
39
|
const {
|
|
23
40
|
AuthenticateWithSharedSecret,
|
|
24
41
|
} = require('../../user/use-cases/authenticate-with-shared-secret');
|
|
@@ -68,8 +85,18 @@ function buildV2Dependencies({ appDefinition, config }) {
|
|
|
68
85
|
|
|
69
86
|
const integrationClasses = appDefinition.integrations || [];
|
|
70
87
|
const userConfig = appDefinition.userConfig || {};
|
|
71
|
-
|
|
72
|
-
|
|
88
|
+
// The registry holds entity types registered on the app as well as those
|
|
89
|
+
// integrations bind (ADR-059), so v2 authorize/entities accept both.
|
|
90
|
+
const registry =
|
|
91
|
+
entityTypeRegistry ??
|
|
92
|
+
appDefinition.entityTypeRegistry ??
|
|
93
|
+
getEntityTypeRegistry(appDefinition);
|
|
94
|
+
const moduleDefinitions = getEntityTypeDefinitions(registry);
|
|
95
|
+
// Ownership by scope (ADR-056 §3): one object every use case asks.
|
|
96
|
+
const scopePolicy = createScopePolicy({
|
|
97
|
+
ownership: appDefinition.ownership,
|
|
98
|
+
entityTypeRegistry: registry,
|
|
99
|
+
});
|
|
73
100
|
|
|
74
101
|
const moduleRepository = createModuleRepository();
|
|
75
102
|
const integrationRepository = createIntegrationRepository();
|
|
@@ -92,6 +119,7 @@ function buildV2Dependencies({ appDefinition, config }) {
|
|
|
92
119
|
const getEntityModuleForUser = new GetEntityModuleForUser({
|
|
93
120
|
moduleRepository,
|
|
94
121
|
moduleDefinitions,
|
|
122
|
+
scopePolicy,
|
|
95
123
|
});
|
|
96
124
|
const getCredentialForUser = new credentialOps.GetCredentialForUserV2({
|
|
97
125
|
credentialRepository,
|
|
@@ -109,17 +137,22 @@ function buildV2Dependencies({ appDefinition, config }) {
|
|
|
109
137
|
moduleDefinitions,
|
|
110
138
|
getAuthorizationRequirements,
|
|
111
139
|
authorizationSessionRepository,
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
140
|
+
authorizeEntityForUser: new AuthorizeEntityForUser({
|
|
141
|
+
scopePolicy,
|
|
142
|
+
processAuthorizationCallback: new ProcessAuthorizationCallback({
|
|
143
|
+
moduleRepository,
|
|
144
|
+
credentialRepository,
|
|
145
|
+
integrationRepository,
|
|
146
|
+
moduleDefinitions,
|
|
147
|
+
}),
|
|
117
148
|
}),
|
|
118
149
|
moduleRepository,
|
|
119
150
|
credentialRepository,
|
|
151
|
+
scopePolicy,
|
|
120
152
|
});
|
|
121
153
|
const getOwnedIntegrationRecord = new integrationOps.GetOwnedIntegrationRecord({
|
|
122
154
|
integrationRepository,
|
|
155
|
+
scopePolicy,
|
|
123
156
|
});
|
|
124
157
|
const getIntegrationInstanceForUser = new integrationOps.GetIntegrationInstanceForUser({
|
|
125
158
|
getOwnedIntegrationRecord,
|
|
@@ -129,6 +162,7 @@ function buildV2Dependencies({ appDefinition, config }) {
|
|
|
129
162
|
return {
|
|
130
163
|
config,
|
|
131
164
|
moduleDefinitions,
|
|
165
|
+
scopePolicy,
|
|
132
166
|
repositories: {
|
|
133
167
|
moduleRepository,
|
|
134
168
|
integrationRepository,
|
|
@@ -144,6 +178,7 @@ function buildV2Dependencies({ appDefinition, config }) {
|
|
|
144
178
|
integrationClasses,
|
|
145
179
|
moduleFactory,
|
|
146
180
|
moduleRepository,
|
|
181
|
+
scopePolicy,
|
|
147
182
|
}),
|
|
148
183
|
getPossibleIntegrations: new GetPossibleIntegrations({ integrationClasses }),
|
|
149
184
|
createIntegrationForUser: new integrationOps.CreateIntegrationForUser({
|
|
@@ -152,7 +187,11 @@ function buildV2Dependencies({ appDefinition, config }) {
|
|
|
152
187
|
integrationClasses,
|
|
153
188
|
moduleFactory,
|
|
154
189
|
}),
|
|
155
|
-
|
|
190
|
+
resolveIntegrationOwnership: new ResolveIntegrationOwnership({
|
|
191
|
+
moduleRepository,
|
|
192
|
+
integrationClasses,
|
|
193
|
+
scopePolicy,
|
|
194
|
+
}),
|
|
156
195
|
integrationClasses,
|
|
157
196
|
}),
|
|
158
197
|
getIntegrationInstanceForUser,
|
|
@@ -184,7 +223,10 @@ function buildV2Dependencies({ appDefinition, config }) {
|
|
|
184
223
|
|
|
185
224
|
// Entities
|
|
186
225
|
getEntityModuleForUser,
|
|
187
|
-
listEntitiesForUser: new entityOps.ListEntitiesForUser({
|
|
226
|
+
listEntitiesForUser: new entityOps.ListEntitiesForUser({
|
|
227
|
+
moduleRepository,
|
|
228
|
+
scopePolicy,
|
|
229
|
+
}),
|
|
188
230
|
getEntityForUser: new entityOps.GetEntityForUser({ getEntityModuleForUser }),
|
|
189
231
|
deleteEntityForUser: new entityOps.DeleteEntityForUser({
|
|
190
232
|
getEntityModuleForUser,
|
|
@@ -196,8 +238,8 @@ function buildV2Dependencies({ appDefinition, config }) {
|
|
|
196
238
|
refreshEntityOptionsForUser: new entityOps.RefreshEntityOptionsForUser({
|
|
197
239
|
getEntityModuleForUser,
|
|
198
240
|
}),
|
|
199
|
-
listEntityTypes: new ListEntityTypes({
|
|
200
|
-
getEntityType: new GetEntityType({
|
|
241
|
+
listEntityTypes: new ListEntityTypes({ entityTypeRegistry: registry, scopePolicy }),
|
|
242
|
+
getEntityType: new GetEntityType({ entityTypeRegistry: registry, scopePolicy }),
|
|
201
243
|
getAuthorizationRequirements,
|
|
202
244
|
|
|
203
245
|
// Credentials
|
package/management-api/v2/dto.js
CHANGED
|
@@ -13,6 +13,20 @@ const idOf = (value) => {
|
|
|
13
13
|
return String(value);
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* `{ scope, owner, organizationId, access }` (ADR-056 §3.4): whose a record is
|
|
18
|
+
* and what the caller may do with it. Empty when the use case did not say.
|
|
19
|
+
*/
|
|
20
|
+
function tenancyFields(tenancy) {
|
|
21
|
+
if (!tenancy) return {};
|
|
22
|
+
return {
|
|
23
|
+
scope: tenancy.scope,
|
|
24
|
+
owner: tenancy.owner,
|
|
25
|
+
organizationId: tenancy.organizationId ?? null,
|
|
26
|
+
access: tenancy.access,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
16
30
|
/** An entity (connected account). */
|
|
17
31
|
function toEntityDto(entity) {
|
|
18
32
|
const credential =
|
|
@@ -30,6 +44,7 @@ function toEntityDto(entity) {
|
|
|
30
44
|
typeof credential?.authIsValid === 'boolean'
|
|
31
45
|
? credential.authIsValid
|
|
32
46
|
: null,
|
|
47
|
+
...tenancyFields(entity.tenancy),
|
|
33
48
|
};
|
|
34
49
|
}
|
|
35
50
|
|
|
@@ -42,6 +57,7 @@ function toIntegrationDto(integration) {
|
|
|
42
57
|
config: integration.config ?? {},
|
|
43
58
|
version: integration.version ?? null,
|
|
44
59
|
messages: integration.messages ?? {},
|
|
60
|
+
...tenancyFields(integration.tenancy),
|
|
45
61
|
};
|
|
46
62
|
}
|
|
47
63
|
|
|
@@ -115,6 +131,7 @@ function maskCredential(credential) {
|
|
|
115
131
|
}
|
|
116
132
|
|
|
117
133
|
module.exports = {
|
|
134
|
+
tenancyFields,
|
|
118
135
|
toEntityDto,
|
|
119
136
|
toIntegrationDto,
|
|
120
137
|
maskCredential,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const { toEntityDto } = require('../dto');
|
|
2
|
-
const { objectBody, parseStep } = require('../request');
|
|
2
|
+
const { objectBody, parseStep, parseListFilters } = require('../request');
|
|
3
3
|
|
|
4
4
|
/** Management API v2 entity and entity-type handlers, keyed by operationId. */
|
|
5
5
|
function createEntityHandlers(useCases) {
|
|
@@ -16,17 +16,20 @@ function createEntityHandlers(useCases) {
|
|
|
16
16
|
} = useCases;
|
|
17
17
|
|
|
18
18
|
return {
|
|
19
|
-
async listEntities(
|
|
20
|
-
const entities = await listEntitiesForUser.execute(
|
|
19
|
+
async listEntities(req, res, user) {
|
|
20
|
+
const entities = await listEntitiesForUser.execute(
|
|
21
|
+
user,
|
|
22
|
+
parseListFilters(req.query)
|
|
23
|
+
);
|
|
21
24
|
res.json({ entities: entities.map(toEntityDto) });
|
|
22
25
|
},
|
|
23
26
|
|
|
24
|
-
async listEntityTypes(_req, res) {
|
|
25
|
-
res.json({ types: await listEntityTypes.execute() });
|
|
27
|
+
async listEntityTypes(_req, res, user) {
|
|
28
|
+
res.json({ types: await listEntityTypes.execute({ user }) });
|
|
26
29
|
},
|
|
27
30
|
|
|
28
|
-
async getEntityType(req, res) {
|
|
29
|
-
res.json(await getEntityType.execute(req.params.entityType));
|
|
31
|
+
async getEntityType(req, res, user) {
|
|
32
|
+
res.json(await getEntityType.execute(req.params.entityType, { user }));
|
|
30
33
|
},
|
|
31
34
|
|
|
32
35
|
// Describes a step's requirements without starting an authorization
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const { toIntegrationDto } = require('../dto');
|
|
2
|
-
const { objectBody, optionalString } = require('../request');
|
|
2
|
+
const { objectBody, optionalString, parseListFilters } = require('../request');
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Management API v2 integration handlers, keyed by registry operationId.
|
|
@@ -27,8 +27,11 @@ function createIntegrationHandlers(useCases) {
|
|
|
27
27
|
);
|
|
28
28
|
|
|
29
29
|
return {
|
|
30
|
-
async listIntegrations(
|
|
31
|
-
const integrations = await getIntegrationsForUser.execute(
|
|
30
|
+
async listIntegrations(req, res, user) {
|
|
31
|
+
const integrations = await getIntegrationsForUser.execute(
|
|
32
|
+
user,
|
|
33
|
+
parseListFilters(req.query)
|
|
34
|
+
);
|
|
32
35
|
res.json({ integrations: integrations.map(toIntegrationDto) });
|
|
33
36
|
},
|
|
34
37
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const Boom = require('@hapi/boom');
|
|
2
|
+
const { LIST_OWNERS } = require('../../modules/scope-visibility');
|
|
2
3
|
|
|
3
4
|
/** The JSON body as an object, or a 400 VALIDATION_ERROR. */
|
|
4
5
|
function objectBody(req) {
|
|
@@ -51,4 +52,34 @@ function optionalString(value, name) {
|
|
|
51
52
|
return value;
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
|
|
55
|
+
const LIST_SCOPES = ['individual', 'organization'];
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* `?scope=individual|organization` and `?owner=me|org` on a list (ADR-056
|
|
59
|
+
* §3.4). Anything else is a 400.
|
|
60
|
+
*/
|
|
61
|
+
function parseListFilters(query = {}) {
|
|
62
|
+
const scope = optionalString(query.scope, 'scope');
|
|
63
|
+
const owner = optionalString(query.owner, 'owner');
|
|
64
|
+
if (scope !== undefined && !LIST_SCOPES.includes(scope)) {
|
|
65
|
+
throw Boom.badRequest(`scope must be one of ${LIST_SCOPES.join(', ')}`, {
|
|
66
|
+
code: 'VALIDATION_ERROR',
|
|
67
|
+
details: { field: 'scope' },
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
if (owner !== undefined && !Object.hasOwn(LIST_OWNERS, owner)) {
|
|
71
|
+
throw Boom.badRequest('owner must be me or org', {
|
|
72
|
+
code: 'VALIDATION_ERROR',
|
|
73
|
+
details: { field: 'owner' },
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return { scope, owner };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = {
|
|
80
|
+
objectBody,
|
|
81
|
+
requireFields,
|
|
82
|
+
parseStep,
|
|
83
|
+
optionalString,
|
|
84
|
+
parseListFilters,
|
|
85
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entity scopes and ownership (ADR-056 §3), re-exported from the four files it
|
|
3
|
+
* is split into:
|
|
4
|
+
*
|
|
5
|
+
* scope-resolution constants, scope resolution, ownership on create
|
|
6
|
+
* scope-config reading and validating app-definition settings
|
|
7
|
+
* scope-visibility who may see which stored row
|
|
8
|
+
* scope-policy the object a use case asks
|
|
9
|
+
*/
|
|
10
|
+
module.exports = {
|
|
11
|
+
...require('./scope-resolution'),
|
|
12
|
+
...require('./scope-config'),
|
|
13
|
+
...require('./scope-visibility'),
|
|
14
|
+
...require('./scope-policy'),
|
|
15
|
+
};
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App-level entity type registry (ADR-059).
|
|
3
|
+
*
|
|
4
|
+
* Pure: reads an app definition, does no I/O. It answers "which entity types
|
|
5
|
+
* (API modules) can this app connect?" from two sources:
|
|
6
|
+
*
|
|
7
|
+
* 1. `appDefinition.entities`, an object keyed by entity type name whose
|
|
8
|
+
* values are `{ definition, scope?, proxy?, display? }`.
|
|
9
|
+
* 2. Inference: every module bound by `appDefinition.integrations[*]
|
|
10
|
+
* .Definition.modules` (skipped when `entities.$infer === false`).
|
|
11
|
+
*
|
|
12
|
+
* `loadAppDefinition()` builds the registry once, at load, and returns it as
|
|
13
|
+
* `entityTypeRegistry` / `moduleDefinitions`; every consumer that holds a
|
|
14
|
+
* loaded app definition reads those instead of rebuilding.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const { BaseError } = require('../errors/base-error');
|
|
18
|
+
const { getLogger } = require('../logs');
|
|
19
|
+
|
|
20
|
+
const log = getLogger('frigg.modules.entity_registry');
|
|
21
|
+
|
|
22
|
+
const INFER_KEY = '$infer';
|
|
23
|
+
const UNNAMED = '(unnamed definition)';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Raised for an invalid `entities` registry. `code` is always
|
|
27
|
+
* `ENTITY_REGISTRY_INVALID`, so callers can tell it from other load errors.
|
|
28
|
+
*/
|
|
29
|
+
class EntityRegistryError extends BaseError {
|
|
30
|
+
constructor(message, options) {
|
|
31
|
+
super(message, options);
|
|
32
|
+
this.code = 'ENTITY_REGISTRY_INVALID';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const isPlainObject = (value) =>
|
|
37
|
+
value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
38
|
+
|
|
39
|
+
const appSource = () => ({ kind: 'app' });
|
|
40
|
+
const integrationSource = (integrationClass) => ({
|
|
41
|
+
kind: 'integration',
|
|
42
|
+
name: integrationClass?.Definition?.name || 'unnamed',
|
|
43
|
+
});
|
|
44
|
+
const describeSource = (source) =>
|
|
45
|
+
source.kind === 'app' ? 'app' : `integration:${source.name}`;
|
|
46
|
+
|
|
47
|
+
const safeGetName = (definition) => {
|
|
48
|
+
if (typeof definition.getName !== 'function') return undefined;
|
|
49
|
+
try {
|
|
50
|
+
const name = definition.getName();
|
|
51
|
+
return typeof name === 'string' && name ? name : undefined;
|
|
52
|
+
} catch {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const moduleNameOf = (definition) =>
|
|
58
|
+
typeof definition?.moduleName === 'string' && definition.moduleName
|
|
59
|
+
? definition.moduleName
|
|
60
|
+
: undefined;
|
|
61
|
+
|
|
62
|
+
/** The name an entity type is known by: moduleName, else getName(). */
|
|
63
|
+
function entityTypeNameOf(definition) {
|
|
64
|
+
if (!definition) return undefined;
|
|
65
|
+
return moduleNameOf(definition) ?? safeGetName(definition);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Same object, or the same package name and version when both can be read. */
|
|
69
|
+
function isSameDefinition(a, b) {
|
|
70
|
+
if (a === b) return true;
|
|
71
|
+
const packageId = (definition) => {
|
|
72
|
+
const { packageName, version } = definition || {};
|
|
73
|
+
return typeof packageName === 'string' &&
|
|
74
|
+
packageName &&
|
|
75
|
+
typeof version === 'string' &&
|
|
76
|
+
version
|
|
77
|
+
? `${packageName}@${version}`
|
|
78
|
+
: null;
|
|
79
|
+
};
|
|
80
|
+
const left = packageId(a);
|
|
81
|
+
return left !== null && left === packageId(b);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function conflictError(name, existing, source) {
|
|
85
|
+
return new EntityRegistryError(
|
|
86
|
+
`Entity type '${name}' is supplied by two different definitions: ` +
|
|
87
|
+
`${existing.sources
|
|
88
|
+
.map(describeSource)
|
|
89
|
+
.join(', ')} and ${describeSource(source)}. ` +
|
|
90
|
+
'They must be the same definition object (or the same package name and version).'
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* A registry key must equal the definition's moduleName and, when it has one,
|
|
96
|
+
* its getName(). Case matters. This leaves one name per entity type.
|
|
97
|
+
*/
|
|
98
|
+
function assertKeyMatchesDefinition(key, definition) {
|
|
99
|
+
const moduleName = moduleNameOf(definition);
|
|
100
|
+
const getName = safeGetName(definition);
|
|
101
|
+
const matches =
|
|
102
|
+
(moduleName !== undefined || getName !== undefined) &&
|
|
103
|
+
(moduleName === undefined || key === moduleName) &&
|
|
104
|
+
(getName === undefined || key === getName);
|
|
105
|
+
if (!matches) {
|
|
106
|
+
throw new EntityRegistryError(
|
|
107
|
+
`appDefinition.entities key '${key}' does not match its definition's name ` +
|
|
108
|
+
`(moduleName '${moduleName ?? '(none)'}', getName() '${
|
|
109
|
+
getName ?? '(none)'
|
|
110
|
+
}'). ` +
|
|
111
|
+
'The key must equal both, so each entity type has one name.'
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const settingsOf = (entry) => {
|
|
117
|
+
const settings = {};
|
|
118
|
+
for (const field of ['scope', 'proxy', 'display']) {
|
|
119
|
+
if (entry[field] !== undefined) settings[field] = entry[field];
|
|
120
|
+
}
|
|
121
|
+
return settings;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* `entities` absent keeps today's behaviour: inference on, and a name clash
|
|
126
|
+
* between integrations is not fatal. `entities` present (even `{}`) opts in to
|
|
127
|
+
* the strict rules.
|
|
128
|
+
*/
|
|
129
|
+
function readEntities(appDefinition) {
|
|
130
|
+
const entities = appDefinition?.entities;
|
|
131
|
+
if (entities === undefined || entities === null) {
|
|
132
|
+
return { strict: false, infer: true, entries: [] };
|
|
133
|
+
}
|
|
134
|
+
if (!isPlainObject(entities)) {
|
|
135
|
+
throw new EntityRegistryError(
|
|
136
|
+
'appDefinition.entities must be an object keyed by entity type name'
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
const { [INFER_KEY]: inferFlag, ...rest } = entities;
|
|
140
|
+
if (inferFlag !== undefined && typeof inferFlag !== 'boolean') {
|
|
141
|
+
throw new EntityRegistryError(
|
|
142
|
+
`appDefinition.entities.${INFER_KEY} must be a boolean`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
strict: true,
|
|
147
|
+
infer: inferFlag !== false,
|
|
148
|
+
entries: Object.entries(rest),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function registerAppEntries(registry, entries) {
|
|
153
|
+
for (const [key, entry] of entries) {
|
|
154
|
+
if (
|
|
155
|
+
!isPlainObject(entry) ||
|
|
156
|
+
!entry.definition ||
|
|
157
|
+
typeof entry.definition !== 'object'
|
|
158
|
+
) {
|
|
159
|
+
throw new EntityRegistryError(
|
|
160
|
+
`appDefinition.entities.${key} must be an object with a 'definition'`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
assertKeyMatchesDefinition(key, entry.definition);
|
|
164
|
+
registry.set(key, {
|
|
165
|
+
definition: entry.definition,
|
|
166
|
+
sources: [appSource()],
|
|
167
|
+
settings: settingsOf(entry),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function* boundDefinitions(integrationClass) {
|
|
173
|
+
const modules = integrationClass?.Definition?.modules;
|
|
174
|
+
if (!isPlainObject(modules)) return;
|
|
175
|
+
for (const bound of Object.values(modules)) {
|
|
176
|
+
if (bound?.definition) yield bound.definition;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const addSource = (entry, source) => {
|
|
181
|
+
const known = entry.sources.some(
|
|
182
|
+
(s) => s.kind === source.kind && s.name === source.name
|
|
183
|
+
);
|
|
184
|
+
if (!known) entry.sources.push(source);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Folds one integration-bound definition into the registry. Returns the name
|
|
189
|
+
* it could not register (only possible when inference is off), else undefined.
|
|
190
|
+
*/
|
|
191
|
+
function mergeBoundDefinition(registry, definition, source, { strict, infer }) {
|
|
192
|
+
const name = entityTypeNameOf(definition);
|
|
193
|
+
|
|
194
|
+
if (!name) {
|
|
195
|
+
// No moduleName / getName(): it cannot be keyed by name. Offering it
|
|
196
|
+
// as before, de-duplicated by identity, keeps apps that never named
|
|
197
|
+
// one working. It cannot be registered, so `$infer: false` rejects it.
|
|
198
|
+
if (!infer) return UNNAMED;
|
|
199
|
+
const known = [...registry.values()].find(
|
|
200
|
+
(entry) => entry.definition === definition
|
|
201
|
+
);
|
|
202
|
+
if (known) addSource(known, source);
|
|
203
|
+
else {
|
|
204
|
+
registry.set(`${UNNAMED} #${registry.size}`, {
|
|
205
|
+
definition,
|
|
206
|
+
sources: [source],
|
|
207
|
+
settings: {},
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const existing = registry.get(name);
|
|
214
|
+
if (existing) {
|
|
215
|
+
if (isSameDefinition(existing.definition, definition)) {
|
|
216
|
+
addSource(existing, source);
|
|
217
|
+
} else if (strict) {
|
|
218
|
+
throw conflictError(name, existing, source);
|
|
219
|
+
} else {
|
|
220
|
+
// Pre-ADR-059 apps keep working: the first definition wins, as
|
|
221
|
+
// ModuleFactory would have picked it.
|
|
222
|
+
log.warn('Two different definitions share one entity type name', {
|
|
223
|
+
eventName: `${log.name}.duplicate_definition`,
|
|
224
|
+
entityType: name,
|
|
225
|
+
kept: existing.sources.map(describeSource).join(', '),
|
|
226
|
+
ignored: describeSource(source),
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (!infer) return name;
|
|
233
|
+
registry.set(name, { definition, sources: [source], settings: {} });
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function assertAllRegistered(unregistered) {
|
|
238
|
+
if (unregistered.size === 0) return;
|
|
239
|
+
const detail = [...unregistered]
|
|
240
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
241
|
+
.map(
|
|
242
|
+
([name, sources]) => `${name} (bound by ${[...sources].join(', ')})`
|
|
243
|
+
);
|
|
244
|
+
throw new EntityRegistryError(
|
|
245
|
+
`appDefinition.entities.${INFER_KEY} is false, so every module an integration binds must be registered. Missing: ${detail.join(
|
|
246
|
+
'; '
|
|
247
|
+
)}`
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const freezeEntries = (registry) => {
|
|
252
|
+
for (const entry of registry.values()) {
|
|
253
|
+
entry.sources.forEach(Object.freeze);
|
|
254
|
+
Object.freeze(entry.sources);
|
|
255
|
+
Object.freeze(entry.settings);
|
|
256
|
+
Object.freeze(entry);
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Builds the registry for an app definition.
|
|
262
|
+
*
|
|
263
|
+
* @param {object} [appDefinition] - `{ entities?, integrations? }`
|
|
264
|
+
* @returns {Map<string, {definition: object, sources: Array<{kind: 'app'} | {kind: 'integration', name: string}>, settings: {scope?: string|object, proxy?: object, display?: object}}>}
|
|
265
|
+
* @throws {EntityRegistryError} on a key/name mismatch, two different
|
|
266
|
+
* definitions under one name (when `entities` is present), or, with
|
|
267
|
+
* `$infer: false`, a bound module that is not registered.
|
|
268
|
+
*/
|
|
269
|
+
function getEntityTypeRegistry(appDefinition = {}) {
|
|
270
|
+
const mode = readEntities(appDefinition);
|
|
271
|
+
const registry = new Map();
|
|
272
|
+
registerAppEntries(registry, mode.entries);
|
|
273
|
+
|
|
274
|
+
const unregistered = new Map();
|
|
275
|
+
for (const integrationClass of appDefinition?.integrations || []) {
|
|
276
|
+
const source = integrationSource(integrationClass);
|
|
277
|
+
for (const definition of boundDefinitions(integrationClass)) {
|
|
278
|
+
const missing = mergeBoundDefinition(
|
|
279
|
+
registry,
|
|
280
|
+
definition,
|
|
281
|
+
source,
|
|
282
|
+
mode
|
|
283
|
+
);
|
|
284
|
+
if (missing !== undefined) {
|
|
285
|
+
if (!unregistered.has(missing))
|
|
286
|
+
unregistered.set(missing, new Set());
|
|
287
|
+
unregistered.get(missing).add(describeSource(source));
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
assertAllRegistered(unregistered);
|
|
292
|
+
|
|
293
|
+
freezeEntries(registry);
|
|
294
|
+
return registry;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** The definitions of a registry, in registry order. */
|
|
298
|
+
const getEntityTypeDefinitions = (registry) =>
|
|
299
|
+
[...registry.values()].map((entry) => entry.definition);
|
|
300
|
+
|
|
301
|
+
/** Names of the integrations that bind an entity type; empty for registry-only. */
|
|
302
|
+
const usedByOf = (entry) =>
|
|
303
|
+
entry.sources.filter((s) => s.kind === 'integration').map((s) => s.name);
|
|
304
|
+
|
|
305
|
+
module.exports = {
|
|
306
|
+
getEntityTypeRegistry,
|
|
307
|
+
getEntityTypeDefinitions,
|
|
308
|
+
usedByOf,
|
|
309
|
+
entityTypeNameOf,
|
|
310
|
+
EntityRegistryError,
|
|
311
|
+
};
|
package/modules/index.js
CHANGED
|
@@ -4,6 +4,15 @@ const { OAuth2Requester } = require('./requester/oauth-2');
|
|
|
4
4
|
const { Requester } = require('./requester/requester');
|
|
5
5
|
const { ModuleConstants } = require('./ModuleConstants');
|
|
6
6
|
const { ModuleFactory } = require('./module-factory');
|
|
7
|
+
const {
|
|
8
|
+
getEntityTypeRegistry,
|
|
9
|
+
EntityRegistryError,
|
|
10
|
+
} = require('./entity-type-registry');
|
|
11
|
+
const {
|
|
12
|
+
resolveScope,
|
|
13
|
+
listBindingScopes,
|
|
14
|
+
OwnershipConfigError,
|
|
15
|
+
} = require('./entity-scope');
|
|
7
16
|
|
|
8
17
|
module.exports = {
|
|
9
18
|
ApiKeyRequester,
|
|
@@ -12,4 +21,9 @@ module.exports = {
|
|
|
12
21
|
Requester,
|
|
13
22
|
ModuleConstants,
|
|
14
23
|
ModuleFactory,
|
|
24
|
+
getEntityTypeRegistry,
|
|
25
|
+
EntityRegistryError,
|
|
26
|
+
resolveScope,
|
|
27
|
+
listBindingScopes,
|
|
28
|
+
OwnershipConfigError,
|
|
15
29
|
};
|