@friggframework/core 2.0.0--canary.678.58db4b2.0 → 2.0.0--canary.680.b3ca7f1.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 +11 -0
- package/CLAUDE.md +0 -2
- package/README.md +0 -57
- package/application/commands/integration-commands.js +18 -8
- package/application/index.js +6 -2
- package/database/encryption/README.md +2 -0
- package/database/encryption/encryption-schema-registry.js +35 -12
- package/database/encryption/field-encryption-service.js +30 -3
- package/database/encryption/test-support/fake-ciphertext.js +15 -0
- package/handlers/app-definition-loader.js +16 -13
- 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 +4 -2
- package/integrations/index.js +0 -4
- package/integrations/integration-router.js +16 -29
- package/integrations/utils/map-integration-dto.js +1 -15
- package/management-api/openapi/components.js +7 -1
- package/management-api/openapi/v2-operations.js +4 -0
- package/management-api/v2/dependencies.js +23 -13
- package/modules/entity-type-registry.js +311 -0
- package/modules/index.js +6 -0
- package/modules/use-cases/entity-types.js +36 -12
- package/package.json +5 -6
- package/types/index.d.ts +1 -1
- package/types/modules/index.d.ts +58 -0
- package/user/use-cases/get-user-from-adopter-jwt.js +122 -109
- package/user/use-cases/get-user-from-x-frigg-headers.js +51 -29
- package/user/user.js +0 -11
- package/types/user/index.d.ts +0 -67
- package/user/adopter-jwt-config.js +0 -177
- package/user/adopter-jwt-errors.js +0 -65
- package/user/adopter-token-verifier.js +0 -122
- package/user/app-user-helpers.js +0 -73
- package/user/tests/doubles/adopter-jwt-fixtures.js +0 -130
|
@@ -31,9 +31,6 @@ const {
|
|
|
31
31
|
GetIntegrationInstance,
|
|
32
32
|
} = require('./use-cases/get-integration-instance');
|
|
33
33
|
const { UpdateIntegration } = require('./use-cases/update-integration');
|
|
34
|
-
const {
|
|
35
|
-
getModulesDefinitionFromIntegrationClasses,
|
|
36
|
-
} = require('./utils/map-integration-dto');
|
|
37
34
|
const {
|
|
38
35
|
GetModuleInstanceFromType,
|
|
39
36
|
} = require('../modules/use-cases/get-module-instance-from-type');
|
|
@@ -63,9 +60,6 @@ const {
|
|
|
63
60
|
const {
|
|
64
61
|
GetUserFromAdopterJwt,
|
|
65
62
|
} = require('../user/use-cases/get-user-from-adopter-jwt');
|
|
66
|
-
const {
|
|
67
|
-
createAdopterTokenVerifier,
|
|
68
|
-
} = require('../user/adopter-token-verifier');
|
|
69
63
|
const {
|
|
70
64
|
AuthenticateWithSharedSecret,
|
|
71
65
|
} = require('../user/use-cases/authenticate-with-shared-secret');
|
|
@@ -77,8 +71,13 @@ const {
|
|
|
77
71
|
const log = getLogger('frigg.integrations');
|
|
78
72
|
|
|
79
73
|
function createIntegrationRouter() {
|
|
80
|
-
|
|
81
|
-
|
|
74
|
+
// moduleDefinitions is the app registry's definitions (ADR-059), so
|
|
75
|
+
// /api/authorize and friends accept a type no integration binds.
|
|
76
|
+
const {
|
|
77
|
+
integrations: integrationClasses,
|
|
78
|
+
userConfig,
|
|
79
|
+
moduleDefinitions,
|
|
80
|
+
} = loadAppDefinition();
|
|
82
81
|
const moduleRepository = createModuleRepository();
|
|
83
82
|
const integrationRepository = createIntegrationRepository();
|
|
84
83
|
const credentialRepository = createCredentialRepository();
|
|
@@ -97,9 +96,6 @@ function createIntegrationRouter() {
|
|
|
97
96
|
const getUserFromAdopterJwt = new GetUserFromAdopterJwt({
|
|
98
97
|
userRepository,
|
|
99
98
|
userConfig,
|
|
100
|
-
tokenVerifier: createAdopterTokenVerifier({
|
|
101
|
-
jwtConfig: userConfig?.jwtConfig,
|
|
102
|
-
}),
|
|
103
99
|
});
|
|
104
100
|
|
|
105
101
|
const authenticateWithSharedSecret = new AuthenticateWithSharedSecret();
|
|
@@ -114,8 +110,7 @@ function createIntegrationRouter() {
|
|
|
114
110
|
|
|
115
111
|
const moduleFactory = new ModuleFactory({
|
|
116
112
|
moduleRepository,
|
|
117
|
-
moduleDefinitions
|
|
118
|
-
getModulesDefinitionFromIntegrationClasses(integrationClasses),
|
|
113
|
+
moduleDefinitions,
|
|
119
114
|
});
|
|
120
115
|
const deleteIntegrationForUser = new DeleteIntegrationForUser({
|
|
121
116
|
integrationRepository,
|
|
@@ -142,8 +137,7 @@ function createIntegrationRouter() {
|
|
|
142
137
|
|
|
143
138
|
const getEntitiesForUser = new GetEntitiesForUser({
|
|
144
139
|
moduleRepository,
|
|
145
|
-
moduleDefinitions
|
|
146
|
-
getModulesDefinitionFromIntegrationClasses(integrationClasses),
|
|
140
|
+
moduleDefinitions,
|
|
147
141
|
});
|
|
148
142
|
|
|
149
143
|
const getIntegrationInstance = new GetIntegrationInstance({
|
|
@@ -159,37 +153,31 @@ function createIntegrationRouter() {
|
|
|
159
153
|
});
|
|
160
154
|
|
|
161
155
|
const getModuleInstanceFromType = new GetModuleInstanceFromType({
|
|
162
|
-
moduleDefinitions
|
|
163
|
-
getModulesDefinitionFromIntegrationClasses(integrationClasses),
|
|
156
|
+
moduleDefinitions,
|
|
164
157
|
});
|
|
165
158
|
|
|
166
159
|
const getEntityOptionsByType = new GetEntityOptionsByType({
|
|
167
|
-
moduleDefinitions
|
|
168
|
-
getModulesDefinitionFromIntegrationClasses(integrationClasses),
|
|
160
|
+
moduleDefinitions,
|
|
169
161
|
});
|
|
170
162
|
|
|
171
163
|
const testModuleAuth = new TestModuleAuth({
|
|
172
164
|
moduleRepository,
|
|
173
|
-
moduleDefinitions
|
|
174
|
-
getModulesDefinitionFromIntegrationClasses(integrationClasses),
|
|
165
|
+
moduleDefinitions,
|
|
175
166
|
});
|
|
176
167
|
|
|
177
168
|
const getModule = new GetModule({
|
|
178
169
|
moduleRepository,
|
|
179
|
-
moduleDefinitions
|
|
180
|
-
getModulesDefinitionFromIntegrationClasses(integrationClasses),
|
|
170
|
+
moduleDefinitions,
|
|
181
171
|
});
|
|
182
172
|
|
|
183
173
|
const getEntityOptionsById = new GetEntityOptionsById({
|
|
184
174
|
moduleRepository,
|
|
185
|
-
moduleDefinitions
|
|
186
|
-
getModulesDefinitionFromIntegrationClasses(integrationClasses),
|
|
175
|
+
moduleDefinitions,
|
|
187
176
|
});
|
|
188
177
|
|
|
189
178
|
const refreshEntityOptions = new RefreshEntityOptions({
|
|
190
179
|
moduleRepository,
|
|
191
|
-
moduleDefinitions
|
|
192
|
-
getModulesDefinitionFromIntegrationClasses(integrationClasses),
|
|
180
|
+
moduleDefinitions,
|
|
193
181
|
});
|
|
194
182
|
|
|
195
183
|
const getPossibleIntegrations = new GetPossibleIntegrations({
|
|
@@ -200,8 +188,7 @@ function createIntegrationRouter() {
|
|
|
200
188
|
moduleRepository,
|
|
201
189
|
credentialRepository,
|
|
202
190
|
integrationRepository,
|
|
203
|
-
moduleDefinitions
|
|
204
|
-
getModulesDefinitionFromIntegrationClasses(integrationClasses),
|
|
191
|
+
moduleDefinitions,
|
|
205
192
|
});
|
|
206
193
|
|
|
207
194
|
const router = express();
|
|
@@ -20,18 +20,4 @@ function mapIntegrationClassToIntegrationDTO(integration) {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
return [
|
|
25
|
-
...new Set(
|
|
26
|
-
integrationClasses
|
|
27
|
-
.map((integration) =>
|
|
28
|
-
Object.values(integration.Definition.modules).map(
|
|
29
|
-
(module) => module.definition
|
|
30
|
-
)
|
|
31
|
-
)
|
|
32
|
-
.flat()
|
|
33
|
-
),
|
|
34
|
-
];
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
module.exports = { mapIntegrationClassToIntegrationDTO, getModulesDefinitionFromIntegrationClasses };
|
|
23
|
+
module.exports = { mapIntegrationClassToIntegrationDTO };
|
|
@@ -138,11 +138,17 @@ const SCHEMAS = {
|
|
|
138
138
|
},
|
|
139
139
|
EntityType: {
|
|
140
140
|
type: 'object',
|
|
141
|
-
required: ['type', 'name', 'authType', 'isMultiStep', 'stepCount'],
|
|
141
|
+
required: ['type', 'name', 'authType', 'isMultiStep', 'stepCount', 'usedBy'],
|
|
142
142
|
properties: {
|
|
143
143
|
type: { type: 'string' },
|
|
144
144
|
name: { type: 'string' },
|
|
145
145
|
description: { type: 'string' },
|
|
146
|
+
usedBy: {
|
|
147
|
+
type: 'array',
|
|
148
|
+
items: { type: 'string' },
|
|
149
|
+
description:
|
|
150
|
+
'Names of the integrations that bind this entity type. Empty for a type registered only in appDefinition.entities.',
|
|
151
|
+
},
|
|
146
152
|
authType: { type: 'string', description: 'oauth2, oauth1, api-key, basic or custom' },
|
|
147
153
|
isMultiStep: { type: 'boolean' },
|
|
148
154
|
stepCount: { type: 'integer', minimum: 1 },
|
|
@@ -112,10 +112,14 @@ const V2_OPERATIONS = {
|
|
|
112
112
|
},
|
|
113
113
|
listEntityTypes: {
|
|
114
114
|
summary: 'List the entity types (API modules) this app can connect',
|
|
115
|
+
description:
|
|
116
|
+
'Every type registered in appDefinition.entities plus every module a declared integration binds. `usedBy` lists the integrations that bind each type; it is empty for a type registered without any integration.',
|
|
115
117
|
responses: { 200: ok('Entity types', ref('ListEntityTypesResponse')), ...errors('Unauthorized') },
|
|
116
118
|
},
|
|
117
119
|
getEntityType: {
|
|
118
120
|
summary: 'Describe an entity type',
|
|
121
|
+
description:
|
|
122
|
+
'Includes `usedBy`, the integrations that bind the type (empty for a registry-only type).',
|
|
119
123
|
responses: { 200: ok('Entity type', ref('EntityType')), ...errors('Unauthorized', 'NotFound') },
|
|
120
124
|
},
|
|
121
125
|
getEntityTypeRequirements: {
|
|
@@ -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,12 +26,12 @@ 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');
|
|
22
32
|
const { GetUserFromBearerToken } = require('../../user/use-cases/get-user-from-bearer-token');
|
|
23
33
|
const { GetUserFromXFriggHeaders } = require('../../user/use-cases/get-user-from-x-frigg-headers');
|
|
24
34
|
const { GetUserFromAdopterJwt } = require('../../user/use-cases/get-user-from-adopter-jwt');
|
|
25
|
-
const { createAdopterTokenVerifier } = require('../../user/adopter-token-verifier');
|
|
26
35
|
const {
|
|
27
36
|
AuthenticateWithSharedSecret,
|
|
28
37
|
} = require('../../user/use-cases/authenticate-with-shared-secret');
|
|
@@ -71,8 +80,13 @@ function buildV2Dependencies({ appDefinition, config }) {
|
|
|
71
80
|
|
|
72
81
|
const integrationClasses = appDefinition.integrations || [];
|
|
73
82
|
const userConfig = appDefinition.userConfig || {};
|
|
74
|
-
|
|
75
|
-
|
|
83
|
+
// The registry holds entity types registered on the app as well as those
|
|
84
|
+
// integrations bind (ADR-059), so v2 authorize/entities accept both.
|
|
85
|
+
const registry =
|
|
86
|
+
entityTypeRegistry ??
|
|
87
|
+
appDefinition.entityTypeRegistry ??
|
|
88
|
+
getEntityTypeRegistry(appDefinition);
|
|
89
|
+
const moduleDefinitions = getEntityTypeDefinitions(registry);
|
|
76
90
|
|
|
77
91
|
const moduleRepository = createModuleRepository();
|
|
78
92
|
const integrationRepository = createIntegrationRepository();
|
|
@@ -83,11 +97,7 @@ function buildV2Dependencies({ appDefinition, config }) {
|
|
|
83
97
|
const authenticateUser = new AuthenticateUser({
|
|
84
98
|
getUserFromBearerToken: new GetUserFromBearerToken({ userRepository, userConfig }),
|
|
85
99
|
getUserFromXFriggHeaders: new GetUserFromXFriggHeaders({ userRepository, userConfig }),
|
|
86
|
-
getUserFromAdopterJwt: new GetUserFromAdopterJwt({
|
|
87
|
-
userRepository,
|
|
88
|
-
userConfig,
|
|
89
|
-
tokenVerifier: createAdopterTokenVerifier({ jwtConfig: userConfig.jwtConfig }),
|
|
90
|
-
}),
|
|
100
|
+
getUserFromAdopterJwt: new GetUserFromAdopterJwt({ userRepository, userConfig }),
|
|
91
101
|
authenticateWithSharedSecret: new AuthenticateWithSharedSecret(),
|
|
92
102
|
userConfig,
|
|
93
103
|
});
|
|
@@ -205,8 +215,8 @@ function buildV2Dependencies({ appDefinition, config }) {
|
|
|
205
215
|
refreshEntityOptionsForUser: new entityOps.RefreshEntityOptionsForUser({
|
|
206
216
|
getEntityModuleForUser,
|
|
207
217
|
}),
|
|
208
|
-
listEntityTypes: new ListEntityTypes({
|
|
209
|
-
getEntityType: new GetEntityType({
|
|
218
|
+
listEntityTypes: new ListEntityTypes({ entityTypeRegistry: registry }),
|
|
219
|
+
getEntityType: new GetEntityType({ entityTypeRegistry: registry }),
|
|
210
220
|
getAuthorizationRequirements,
|
|
211
221
|
|
|
212
222
|
// Credentials
|
|
@@ -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,10 @@ 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');
|
|
7
11
|
|
|
8
12
|
module.exports = {
|
|
9
13
|
ApiKeyRequester,
|
|
@@ -12,4 +16,6 @@ module.exports = {
|
|
|
12
16
|
Requester,
|
|
13
17
|
ModuleConstants,
|
|
14
18
|
ModuleFactory,
|
|
19
|
+
getEntityTypeRegistry,
|
|
20
|
+
EntityRegistryError,
|
|
15
21
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const Boom = require('@hapi/boom');
|
|
2
2
|
const { ModuleConstants } = require('../ModuleConstants');
|
|
3
|
+
const { usedByOf } = require('../entity-type-registry');
|
|
3
4
|
|
|
4
5
|
const AUTH_TYPE_BY_REQUESTER = {
|
|
5
6
|
[ModuleConstants.authType.oauth2]: 'oauth2',
|
|
@@ -22,16 +23,16 @@ function getAuthStepCount(definition) {
|
|
|
22
23
|
* definition: display metadata, auth type and step count. Never credentials
|
|
23
24
|
* or env values.
|
|
24
25
|
*/
|
|
25
|
-
function describeEntityType(definition) {
|
|
26
|
+
function describeEntityType(definition, typeName = definition.moduleName) {
|
|
26
27
|
const stepCount = getAuthStepCount(definition);
|
|
27
28
|
const display = definition.display || {};
|
|
28
29
|
const description = {
|
|
29
|
-
type:
|
|
30
|
+
type: typeName,
|
|
30
31
|
name:
|
|
31
32
|
callIfFunction(definition, 'getDisplayName') ??
|
|
32
33
|
display.label ??
|
|
33
34
|
display.name ??
|
|
34
|
-
|
|
35
|
+
typeName,
|
|
35
36
|
authType:
|
|
36
37
|
callIfFunction(definition, 'getAuthType') ??
|
|
37
38
|
AUTH_TYPE_BY_REQUESTER[definition.API?.requesterType] ??
|
|
@@ -58,27 +59,50 @@ function findDefinition(moduleDefinitions, entityType) {
|
|
|
58
59
|
return definition;
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
|
|
62
|
+
const describeEntry = (name, entry) => ({
|
|
63
|
+
// The registry key is the name the API uses, whatever the definition spells.
|
|
64
|
+
...describeEntityType(entry.definition, name),
|
|
65
|
+
usedBy: usedByOf(entry),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
function requireRegistry(entityTypeRegistry) {
|
|
69
|
+
if (!(entityTypeRegistry instanceof Map)) {
|
|
70
|
+
throw new TypeError('entityTypeRegistry is required (see getEntityTypeRegistry)');
|
|
71
|
+
}
|
|
72
|
+
return entityTypeRegistry;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Lists the entity types this app can authorize: registry entries plus every
|
|
77
|
+
* module an integration binds (ADR-059). `usedBy` names the integrations that
|
|
78
|
+
* bind each type; it is empty for a registry-only type.
|
|
79
|
+
*/
|
|
62
80
|
class ListEntityTypes {
|
|
63
|
-
constructor({
|
|
64
|
-
this.
|
|
81
|
+
constructor({ entityTypeRegistry }) {
|
|
82
|
+
this.registry = requireRegistry(entityTypeRegistry);
|
|
65
83
|
}
|
|
66
84
|
|
|
67
85
|
async execute() {
|
|
68
|
-
return this.
|
|
69
|
-
.map(
|
|
86
|
+
return [...this.registry]
|
|
87
|
+
.map(([name, entry]) => describeEntry(name, entry))
|
|
70
88
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
71
89
|
}
|
|
72
90
|
}
|
|
73
91
|
|
|
74
|
-
/** Describes one entity type
|
|
92
|
+
/** Describes one entity type, looked up by its registry key; 404 when absent. */
|
|
75
93
|
class GetEntityType {
|
|
76
|
-
constructor({
|
|
77
|
-
this.
|
|
94
|
+
constructor({ entityTypeRegistry }) {
|
|
95
|
+
this.registry = requireRegistry(entityTypeRegistry);
|
|
78
96
|
}
|
|
79
97
|
|
|
80
98
|
async execute(entityType) {
|
|
81
|
-
|
|
99
|
+
const entry = this.registry.get(entityType);
|
|
100
|
+
if (!entry) {
|
|
101
|
+
throw Boom.notFound(`Entity type '${entityType}' not found`, {
|
|
102
|
+
code: 'ENTITY_TYPE_NOT_FOUND',
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return describeEntry(entityType, entry);
|
|
82
106
|
}
|
|
83
107
|
}
|
|
84
108
|
|