@backstage/plugin-catalog-backend-module-msgraph-incremental 0.0.0-nightly-20260507032228
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 +24 -0
- package/README.md +74 -0
- package/dist/MicrosoftGraphIncrementalEntityProvider.cjs.js +310 -0
- package/dist/MicrosoftGraphIncrementalEntityProvider.cjs.js.map +1 -0
- package/dist/alpha.cjs.js +3 -0
- package/dist/alpha.cjs.js.map +1 -0
- package/dist/alpha.d.ts +2 -0
- package/dist/clientHelpers.cjs.js +42 -0
- package/dist/clientHelpers.cjs.js.map +1 -0
- package/dist/index.cjs.js +14 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +171 -0
- package/dist/module/catalogModuleMicrosoftGraphIncrementalEntityProvider.cjs.js +123 -0
- package/dist/module/catalogModuleMicrosoftGraphIncrementalEntityProvider.cjs.js.map +1 -0
- package/package.json +79 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# @backstage/plugin-catalog-backend-module-msgraph-incremental
|
|
2
|
+
|
|
3
|
+
## 0.0.0-nightly-20260507032228
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- f1279ea: Introduces a cursor-based incremental ingestion provider for Microsoft Graph that processes users and groups one page at a time. Unlike `MicrosoftGraphOrgEntityProvider`, this module never holds the full dataset in memory — each burst processes a single page (up to 999 users or 100 groups). The `@odata.nextLink` cursor is persisted so a pod restart resumes from the last completed page rather than starting over.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies
|
|
12
|
+
- @backstage/catalog-model@0.0.0-nightly-20260507032228
|
|
13
|
+
- @backstage/plugin-catalog-node@0.0.0-nightly-20260507032228
|
|
14
|
+
- @backstage/plugin-catalog-backend-module-incremental-ingestion@0.0.0-nightly-20260507032228
|
|
15
|
+
- @backstage/plugin-catalog-backend-module-msgraph@0.0.0-nightly-20260507032228
|
|
16
|
+
- @backstage/backend-plugin-api@0.0.0-nightly-20260507032228
|
|
17
|
+
- @backstage/config@0.0.0-nightly-20260507032228
|
|
18
|
+
- @backstage/types@1.2.2
|
|
19
|
+
|
|
20
|
+
## 0.1.0-next.0
|
|
21
|
+
|
|
22
|
+
### Minor Changes
|
|
23
|
+
|
|
24
|
+
- f1279ea: Introduces a cursor-based incremental ingestion provider for Microsoft Graph that processes users and groups one page at a time. Unlike `MicrosoftGraphOrgEntityProvider`, this module never holds the full dataset in memory — each burst processes a single page (up to 999 users or 100 groups). The `@odata.nextLink` cursor is persisted so a pod restart resumes from the last completed page rather than starting over.
|
package/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# @backstage/plugin-catalog-backend-module-msgraph-incremental
|
|
2
|
+
|
|
3
|
+
This module incrementally ingests **users** and **groups** from Microsoft Graph
|
|
4
|
+
into the Backstage catalog, one page at a time. It is suitable for large Azure
|
|
5
|
+
AD tenants where holding the full dataset in memory at once is not practical.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Cursor-based resumption** — the `@odata.nextLink` URL is persisted as the
|
|
10
|
+
cursor, so a pod restart during ingestion resumes from the last completed page
|
|
11
|
+
rather than starting over.
|
|
12
|
+
- **Memory-efficient** — each burst processes a single page (up to 999 users
|
|
13
|
+
or 100 groups), keeping memory usage flat regardless of tenant size.
|
|
14
|
+
- **Photo support** — user profile photos are fetched with a gated pre-check to
|
|
15
|
+
avoid unnecessary API calls for users without photos.
|
|
16
|
+
- **Transformer extension point** — user, group, organization, and provider
|
|
17
|
+
config transformers can be customised via the
|
|
18
|
+
`microsoftGraphIncrementalEntityProviderTransformExtensionPoint`.
|
|
19
|
+
|
|
20
|
+
## Prerequisites
|
|
21
|
+
|
|
22
|
+
This module requires the incremental ingestion framework to be installed:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
backend.add(
|
|
26
|
+
import('@backstage/plugin-catalog-backend-module-incremental-ingestion'),
|
|
27
|
+
);
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
// packages/backend/src/index.ts
|
|
34
|
+
backend.add(
|
|
35
|
+
import('@backstage/plugin-catalog-backend-module-incremental-ingestion'),
|
|
36
|
+
);
|
|
37
|
+
backend.add(
|
|
38
|
+
import('@backstage/plugin-catalog-backend-module-msgraph-incremental'),
|
|
39
|
+
);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Configuration
|
|
43
|
+
|
|
44
|
+
Uses the same `catalog.providers.microsoftGraphOrg` configuration as
|
|
45
|
+
`@backstage/plugin-catalog-backend-module-msgraph`. See that package's
|
|
46
|
+
documentation for full config reference.
|
|
47
|
+
|
|
48
|
+
```yaml
|
|
49
|
+
catalog:
|
|
50
|
+
providers:
|
|
51
|
+
microsoftGraphOrg:
|
|
52
|
+
default:
|
|
53
|
+
tenantId: ${AZURE_TENANT_ID}
|
|
54
|
+
clientId: ${AZURE_CLIENT_ID}
|
|
55
|
+
clientSecret: ${AZURE_CLIENT_SECRET}
|
|
56
|
+
queryMode: advanced
|
|
57
|
+
user:
|
|
58
|
+
filter: 'accountEnabled eq true'
|
|
59
|
+
group:
|
|
60
|
+
filter: 'securityEnabled eq true'
|
|
61
|
+
schedule:
|
|
62
|
+
frequency: { hours: 12 }
|
|
63
|
+
timeout: { hours: 4 }
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Differences from `MicrosoftGraphOrgEntityProvider`
|
|
67
|
+
|
|
68
|
+
| | `MicrosoftGraphOrgEntityProvider` | This module |
|
|
69
|
+
| -------------------------- | --------------------------------- | ------------------- |
|
|
70
|
+
| Memory usage | Full dataset in RAM | One page at a time |
|
|
71
|
+
| Resume on restart | Starts from scratch | Resumes from cursor |
|
|
72
|
+
| `userGroupMember*` options | Supported | Not supported |
|
|
73
|
+
| `groupIncludeSubGroups` | Supported | Not supported |
|
|
74
|
+
| Suitable for large tenants | No | Yes |
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var crypto = require('node:crypto');
|
|
4
|
+
var catalogModel = require('@backstage/catalog-model');
|
|
5
|
+
var limiterFactory = require('p-limit');
|
|
6
|
+
var pluginCatalogBackendModuleMsgraph = require('@backstage/plugin-catalog-backend-module-msgraph');
|
|
7
|
+
var clientHelpers = require('./clientHelpers.cjs.js');
|
|
8
|
+
|
|
9
|
+
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
10
|
+
|
|
11
|
+
var crypto__default = /*#__PURE__*/_interopDefaultCompat(crypto);
|
|
12
|
+
var limiterFactory__default = /*#__PURE__*/_interopDefaultCompat(limiterFactory);
|
|
13
|
+
|
|
14
|
+
const USER_PAGE_SIZE = 999;
|
|
15
|
+
const GROUP_PAGE_SIZE = 100;
|
|
16
|
+
function capEntityName(name) {
|
|
17
|
+
if (name.length <= 63) return name;
|
|
18
|
+
const hash = crypto__default.default.createHash("sha1").update(name).digest("hex").slice(0, 8);
|
|
19
|
+
return `${name.slice(0, 54)}_${hash}`;
|
|
20
|
+
}
|
|
21
|
+
function withLocations(providerId, entity) {
|
|
22
|
+
const uid = entity.metadata.annotations?.[pluginCatalogBackendModuleMsgraph.MICROSOFT_GRAPH_USER_ID_ANNOTATION] || entity.metadata.annotations?.[pluginCatalogBackendModuleMsgraph.MICROSOFT_GRAPH_GROUP_ID_ANNOTATION] || entity.metadata.annotations?.[pluginCatalogBackendModuleMsgraph.MICROSOFT_GRAPH_TENANT_ID_ANNOTATION] || entity.metadata.name;
|
|
23
|
+
const location = `msgraph:${providerId}/${encodeURIComponent(uid)}`;
|
|
24
|
+
return {
|
|
25
|
+
...entity,
|
|
26
|
+
metadata: {
|
|
27
|
+
...entity.metadata,
|
|
28
|
+
annotations: {
|
|
29
|
+
...entity.metadata.annotations,
|
|
30
|
+
[catalogModel.ANNOTATION_LOCATION]: location,
|
|
31
|
+
[catalogModel.ANNOTATION_ORIGIN_LOCATION]: location
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
class MicrosoftGraphIncrementalEntityProvider {
|
|
37
|
+
/**
|
|
38
|
+
* Create one provider instance per provider entry in
|
|
39
|
+
* `catalog.providers.microsoftGraphOrg`.
|
|
40
|
+
*/
|
|
41
|
+
static fromConfig(configRoot, options) {
|
|
42
|
+
function getTransformer(id, transformers) {
|
|
43
|
+
if (["undefined", "function"].includes(typeof transformers)) {
|
|
44
|
+
return transformers;
|
|
45
|
+
}
|
|
46
|
+
return transformers[id];
|
|
47
|
+
}
|
|
48
|
+
return pluginCatalogBackendModuleMsgraph.readProviderConfigs(configRoot).map(
|
|
49
|
+
(providerConfig) => new MicrosoftGraphIncrementalEntityProvider({
|
|
50
|
+
id: providerConfig.id,
|
|
51
|
+
provider: providerConfig,
|
|
52
|
+
logger: options.logger,
|
|
53
|
+
userTransformer: getTransformer(
|
|
54
|
+
providerConfig.id,
|
|
55
|
+
options.userTransformer
|
|
56
|
+
),
|
|
57
|
+
groupTransformer: getTransformer(
|
|
58
|
+
providerConfig.id,
|
|
59
|
+
options.groupTransformer
|
|
60
|
+
),
|
|
61
|
+
organizationTransformer: getTransformer(
|
|
62
|
+
providerConfig.id,
|
|
63
|
+
options.organizationTransformer
|
|
64
|
+
),
|
|
65
|
+
providerConfigTransformer: getTransformer(
|
|
66
|
+
providerConfig.id,
|
|
67
|
+
options.providerConfigTransformer
|
|
68
|
+
)
|
|
69
|
+
})
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
options;
|
|
73
|
+
constructor(options) {
|
|
74
|
+
this.options = options;
|
|
75
|
+
}
|
|
76
|
+
/** {@inheritdoc @backstage/plugin-catalog-backend-module-incremental-ingestion#IncrementalEntityProvider.getProviderName} */
|
|
77
|
+
getProviderName() {
|
|
78
|
+
return `MicrosoftGraphIncrementalEntityProvider:${this.options.id}`;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Sets up the Microsoft Graph client for the duration of a full ingestion
|
|
82
|
+
* cycle. The optional `providerConfigTransformer` is applied here so that
|
|
83
|
+
* dynamic config changes (e.g., rotating credentials) take effect at the
|
|
84
|
+
* start of each cycle rather than mid-way through.
|
|
85
|
+
*/
|
|
86
|
+
async around(burst) {
|
|
87
|
+
const provider = this.options.providerConfigTransformer ? await this.options.providerConfigTransformer(this.options.provider) : this.options.provider;
|
|
88
|
+
if (provider.userGroupMemberFilter || provider.userGroupMemberSearch || provider.userGroupMemberPath) {
|
|
89
|
+
this.options.logger.warn(
|
|
90
|
+
`${this.getProviderName()}: userGroupMemberFilter/Search/Path are not supported by MicrosoftGraphIncrementalEntityProvider. Users will be fetched via the standard userFilter/userPath options instead. Switch to MicrosoftGraphOrgEntityProvider if you require userGroupMember-based ingestion.`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
if (provider.groupIncludeSubGroups) {
|
|
94
|
+
this.options.logger.warn(
|
|
95
|
+
`${this.getProviderName()}: groupIncludeSubGroups is not supported by MicrosoftGraphIncrementalEntityProvider and will be ignored. Switch to MicrosoftGraphOrgEntityProvider if you require this option.`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
const client = pluginCatalogBackendModuleMsgraph.MicrosoftGraphClient.create(provider);
|
|
99
|
+
await burst({ client, provider });
|
|
100
|
+
}
|
|
101
|
+
/** {@inheritdoc @backstage/plugin-catalog-backend-module-incremental-ingestion#IncrementalEntityProvider.next} */
|
|
102
|
+
async next({ client, provider }, cursor) {
|
|
103
|
+
const phase = cursor?.phase ?? "users";
|
|
104
|
+
const nextLink = cursor?.nextLink;
|
|
105
|
+
if (phase === "users") {
|
|
106
|
+
return this.readUsersPage(client, provider, nextLink);
|
|
107
|
+
}
|
|
108
|
+
return this.readGroupsPage(client, provider, nextLink);
|
|
109
|
+
}
|
|
110
|
+
async readUsersPage(client, provider, nextLink) {
|
|
111
|
+
const { items: rawUsers, nextLink: newNextLink } = await clientHelpers.requestOnePage(
|
|
112
|
+
client,
|
|
113
|
+
provider.userPath ?? "users",
|
|
114
|
+
{
|
|
115
|
+
query: {
|
|
116
|
+
filter: provider.userFilter,
|
|
117
|
+
expand: provider.userExpand,
|
|
118
|
+
select: provider.userSelect,
|
|
119
|
+
top: USER_PAGE_SIZE
|
|
120
|
+
},
|
|
121
|
+
queryMode: provider.queryMode,
|
|
122
|
+
nextLink
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
|
+
const transformer = this.options.userTransformer ?? pluginCatalogBackendModuleMsgraph.defaultUserTransformer;
|
|
126
|
+
const limiter = limiterFactory__default.default(10);
|
|
127
|
+
const entities = [];
|
|
128
|
+
await Promise.all(
|
|
129
|
+
rawUsers.map(
|
|
130
|
+
(user) => limiter(async () => {
|
|
131
|
+
let userPhoto;
|
|
132
|
+
if (user.id && provider.loadUserPhotos !== false) {
|
|
133
|
+
try {
|
|
134
|
+
userPhoto = await clientHelpers.getUserPhotoGated(client, user.id, 120);
|
|
135
|
+
} catch (e) {
|
|
136
|
+
this.options.logger.debug(
|
|
137
|
+
`${this.getProviderName()}: failed to load photo for user ${user.id}`,
|
|
138
|
+
{ error: e }
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const entity = await transformer(user, userPhoto);
|
|
143
|
+
if (entity) {
|
|
144
|
+
entity.metadata.name = capEntityName(entity.metadata.name);
|
|
145
|
+
entities.push({
|
|
146
|
+
locationKey: `msgraph-org-provider:${this.options.id}`,
|
|
147
|
+
entity: withLocations(this.options.id, entity)
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
)
|
|
152
|
+
);
|
|
153
|
+
this.options.logger.debug(
|
|
154
|
+
`${this.getProviderName()}: read ${entities.length} users`,
|
|
155
|
+
{ phase: "users", hasNextPage: !!newNextLink }
|
|
156
|
+
);
|
|
157
|
+
if (newNextLink) {
|
|
158
|
+
return {
|
|
159
|
+
done: false,
|
|
160
|
+
entities,
|
|
161
|
+
cursor: { phase: "users", nextLink: newNextLink }
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
done: false,
|
|
166
|
+
entities,
|
|
167
|
+
cursor: { phase: "groups" }
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
async readGroupsPage(client, provider, nextLink) {
|
|
171
|
+
const { items: rawGroups, nextLink: newNextLink } = await clientHelpers.requestOnePage(
|
|
172
|
+
client,
|
|
173
|
+
provider.groupPath ?? "groups",
|
|
174
|
+
{
|
|
175
|
+
query: {
|
|
176
|
+
filter: provider.groupFilter,
|
|
177
|
+
search: provider.groupSearch,
|
|
178
|
+
expand: provider.groupExpand,
|
|
179
|
+
select: provider.groupSelect,
|
|
180
|
+
top: GROUP_PAGE_SIZE
|
|
181
|
+
},
|
|
182
|
+
queryMode: provider.queryMode,
|
|
183
|
+
nextLink
|
|
184
|
+
}
|
|
185
|
+
);
|
|
186
|
+
const groupTransformer = this.options.groupTransformer ?? pluginCatalogBackendModuleMsgraph.defaultGroupTransformer;
|
|
187
|
+
const userTransformer = this.options.userTransformer ?? pluginCatalogBackendModuleMsgraph.defaultUserTransformer;
|
|
188
|
+
const limiter = limiterFactory__default.default(10);
|
|
189
|
+
const entities = [];
|
|
190
|
+
if (!nextLink) {
|
|
191
|
+
try {
|
|
192
|
+
const organization = await client.getOrganization(provider.tenantId);
|
|
193
|
+
const orgTransformer = this.options.organizationTransformer ?? pluginCatalogBackendModuleMsgraph.defaultOrganizationTransformer;
|
|
194
|
+
const rootGroup = await orgTransformer(organization);
|
|
195
|
+
if (rootGroup) {
|
|
196
|
+
entities.push({
|
|
197
|
+
locationKey: `msgraph-org-provider:${this.options.id}`,
|
|
198
|
+
entity: withLocations(this.options.id, rootGroup)
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
} catch (e) {
|
|
202
|
+
this.options.logger.warn(
|
|
203
|
+
`${this.getProviderName()}: failed to read organization root group`,
|
|
204
|
+
{ error: e }
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
await Promise.all(
|
|
209
|
+
rawGroups.map(
|
|
210
|
+
(group) => limiter(async () => {
|
|
211
|
+
const entity = await groupTransformer(group);
|
|
212
|
+
if (!entity) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
entity.metadata.name = capEntityName(entity.metadata.name);
|
|
216
|
+
const userRefs = [];
|
|
217
|
+
const childRefs = [];
|
|
218
|
+
for await (const member of client.getGroupMembers(group.id, {
|
|
219
|
+
top: GROUP_PAGE_SIZE,
|
|
220
|
+
// Request the minimum fields needed by defaultUserTransformer and
|
|
221
|
+
// defaultGroupTransformer so member objects are never sparse.
|
|
222
|
+
select: [
|
|
223
|
+
"id",
|
|
224
|
+
"displayName",
|
|
225
|
+
"mail",
|
|
226
|
+
"mailNickname",
|
|
227
|
+
"userPrincipalName",
|
|
228
|
+
"description",
|
|
229
|
+
"securityEnabled"
|
|
230
|
+
]
|
|
231
|
+
})) {
|
|
232
|
+
if (member["@odata.type"] === "#microsoft.graph.user") {
|
|
233
|
+
try {
|
|
234
|
+
const userEntity = await userTransformer(
|
|
235
|
+
member
|
|
236
|
+
);
|
|
237
|
+
if (userEntity) {
|
|
238
|
+
userEntity.metadata.name = capEntityName(
|
|
239
|
+
userEntity.metadata.name
|
|
240
|
+
);
|
|
241
|
+
userRefs.push(catalogModel.stringifyEntityRef(userEntity));
|
|
242
|
+
} else {
|
|
243
|
+
this.options.logger.debug(
|
|
244
|
+
`${this.getProviderName()}: group member user ${member.id} could not be transformed (sparse object?), skipping`
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
} catch (e) {
|
|
248
|
+
this.options.logger.warn(
|
|
249
|
+
`${this.getProviderName()}: group member user ${member.id} failed to transform, skipping`,
|
|
250
|
+
{ error: e }
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
} else if (member["@odata.type"] === "#microsoft.graph.group") {
|
|
254
|
+
if (!provider.groupFilter && !provider.groupSearch) {
|
|
255
|
+
try {
|
|
256
|
+
const childEntity = await groupTransformer(
|
|
257
|
+
member
|
|
258
|
+
);
|
|
259
|
+
if (childEntity) {
|
|
260
|
+
childEntity.metadata.name = capEntityName(
|
|
261
|
+
childEntity.metadata.name
|
|
262
|
+
);
|
|
263
|
+
childRefs.push(catalogModel.stringifyEntityRef(childEntity));
|
|
264
|
+
} else {
|
|
265
|
+
this.options.logger.debug(
|
|
266
|
+
`${this.getProviderName()}: group member child group ${member.id} could not be transformed (sparse object?), skipping`
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
} catch (e) {
|
|
270
|
+
this.options.logger.warn(
|
|
271
|
+
`${this.getProviderName()}: group member child group ${member.id} failed to transform, skipping`,
|
|
272
|
+
{ error: e }
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const existingMembers = Array.isArray(entity.spec?.members) ? entity.spec.members : [];
|
|
279
|
+
const existingChildren = Array.isArray(entity.spec?.children) ? entity.spec.children : [];
|
|
280
|
+
entities.push({
|
|
281
|
+
locationKey: `msgraph-org-provider:${this.options.id}`,
|
|
282
|
+
entity: withLocations(this.options.id, {
|
|
283
|
+
...entity,
|
|
284
|
+
spec: {
|
|
285
|
+
...entity.spec,
|
|
286
|
+
members: [.../* @__PURE__ */ new Set([...existingMembers, ...userRefs])],
|
|
287
|
+
children: [.../* @__PURE__ */ new Set([...existingChildren, ...childRefs])]
|
|
288
|
+
}
|
|
289
|
+
})
|
|
290
|
+
});
|
|
291
|
+
})
|
|
292
|
+
)
|
|
293
|
+
);
|
|
294
|
+
this.options.logger.debug(
|
|
295
|
+
`${this.getProviderName()}: read ${rawGroups.length} groups`,
|
|
296
|
+
{ phase: "groups", hasNextPage: !!newNextLink }
|
|
297
|
+
);
|
|
298
|
+
if (newNextLink) {
|
|
299
|
+
return {
|
|
300
|
+
done: false,
|
|
301
|
+
entities,
|
|
302
|
+
cursor: { phase: "groups", nextLink: newNextLink }
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
return { done: true, entities };
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
exports.MicrosoftGraphIncrementalEntityProvider = MicrosoftGraphIncrementalEntityProvider;
|
|
310
|
+
//# sourceMappingURL=MicrosoftGraphIncrementalEntityProvider.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MicrosoftGraphIncrementalEntityProvider.cjs.js","sources":["../src/MicrosoftGraphIncrementalEntityProvider.ts"],"sourcesContent":["/*\n * Copyright 2026 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport crypto from 'node:crypto';\nimport {\n ANNOTATION_LOCATION,\n ANNOTATION_ORIGIN_LOCATION,\n Entity,\n stringifyEntityRef,\n} from '@backstage/catalog-model';\nimport { Config } from '@backstage/config';\nimport {\n IncrementalEntityProvider,\n EntityIteratorResult,\n} from '@backstage/plugin-catalog-backend-module-incremental-ingestion';\nimport { DeferredEntity } from '@backstage/plugin-catalog-node';\nimport limiterFactory from 'p-limit';\nimport * as MicrosoftGraph from '@microsoft/microsoft-graph-types';\nimport {\n GroupTransformer,\n MICROSOFT_GRAPH_GROUP_ID_ANNOTATION,\n MICROSOFT_GRAPH_TENANT_ID_ANNOTATION,\n MICROSOFT_GRAPH_USER_ID_ANNOTATION,\n MicrosoftGraphClient,\n MicrosoftGraphProviderConfig,\n OrganizationTransformer,\n ProviderConfigTransformer,\n UserTransformer,\n defaultGroupTransformer,\n defaultOrganizationTransformer,\n defaultUserTransformer,\n readProviderConfigs,\n} from '@backstage/plugin-catalog-backend-module-msgraph';\nimport { LoggerService } from '@backstage/backend-plugin-api';\nimport { getUserPhotoGated, requestOnePage } from './clientHelpers';\n\nconst USER_PAGE_SIZE = 999;\n// Groups phase fetches members for every group on the page, so a smaller page\n// size keeps each burst within its time budget.\nconst GROUP_PAGE_SIZE = 100;\n\n/**\n * Backstage entity names must be ≤63 chars ([a-zA-Z0-9] separated by [-_.]).\n * When MS Graph UPNs exceed that (e.g. calendar/booking accounts), we truncate\n * to 54 chars and append an 8-char SHA-1 hash to preserve uniqueness.\n */\nfunction capEntityName(name: string): string {\n if (name.length <= 63) return name;\n const hash = crypto.createHash('sha1').update(name).digest('hex').slice(0, 8);\n return `${name.slice(0, 54)}_${hash}`;\n}\n\n/** Stamps `annotations.backstage.io/location` on an entity using the MS Graph UID. */\nfunction withLocations(providerId: string, entity: Entity): Entity {\n const uid =\n entity.metadata.annotations?.[MICROSOFT_GRAPH_USER_ID_ANNOTATION] ||\n entity.metadata.annotations?.[MICROSOFT_GRAPH_GROUP_ID_ANNOTATION] ||\n entity.metadata.annotations?.[MICROSOFT_GRAPH_TENANT_ID_ANNOTATION] ||\n entity.metadata.name;\n const location = `msgraph:${providerId}/${encodeURIComponent(uid)}`;\n return {\n ...entity,\n metadata: {\n ...entity.metadata,\n annotations: {\n ...entity.metadata.annotations,\n [ANNOTATION_LOCATION]: location,\n [ANNOTATION_ORIGIN_LOCATION]: location,\n },\n },\n };\n}\n\n/**\n * Pagination cursor used by {@link MicrosoftGraphIncrementalEntityProvider}.\n *\n * The `nextLink` field holds the `@odata.nextLink` URL returned by the\n * Microsoft Graph API, which encodes all state needed to resume a paged\n * request. An absent value means the current phase is starting fresh.\n *\n * @public\n */\nexport type MSGraphCursor = {\n phase: 'users' | 'groups';\n nextLink?: string;\n};\n\n/**\n * Context passed to each burst of {@link MicrosoftGraphIncrementalEntityProvider}.\n *\n * @public\n */\nexport type MSGraphContext = {\n client: MicrosoftGraphClient;\n provider: MicrosoftGraphProviderConfig;\n};\n\n/**\n * Options for {@link MicrosoftGraphIncrementalEntityProvider}.\n *\n * @public\n */\nexport interface MicrosoftGraphIncrementalEntityProviderOptions {\n /**\n * The logger to use.\n */\n logger: LoggerService;\n\n /**\n * The function that transforms a user entry in msgraph to an entity.\n * Optionally, you can pass separate transformers per provider ID.\n */\n userTransformer?: UserTransformer | Record<string, UserTransformer>;\n\n /**\n * The function that transforms a group entry in msgraph to an entity.\n * Optionally, you can pass separate transformers per provider ID.\n */\n groupTransformer?: GroupTransformer | Record<string, GroupTransformer>;\n\n /**\n * The function that transforms an organization entry in msgraph to an entity.\n * Optionally, you can pass separate transformers per provider ID.\n */\n organizationTransformer?:\n | OrganizationTransformer\n | Record<string, OrganizationTransformer>;\n\n /**\n * The function that transforms provider config dynamically before each sync.\n * Optionally, you can pass separate transformers per provider ID.\n */\n providerConfigTransformer?:\n | ProviderConfigTransformer\n | Record<string, ProviderConfigTransformer>;\n}\n\n/**\n * Incrementally reads user and group entries out of Microsoft Graph, one page\n * at a time, and provides them as User and Group entities for the catalog.\n *\n * Unlike `MicrosoftGraphOrgEntityProvider`, this provider never holds the full\n * dataset in memory at once. Each burst processes a single page (up to 999\n * users or 100 groups). This makes it suitable for very large tenants and\n * avoids the memory pressure and long-running task issues of the full-scan\n * provider.\n *\n * The Microsoft Graph `@odata.nextLink` URL is stored as the cursor, so a pod\n * restart during ingestion resumes from the last completed page.\n *\n * Group membership (`spec.members`) is resolved inline during the groups phase\n * by fetching the direct members of each group. The catalog's built-in relation\n * stitching derives `spec.memberOf` on users from these group membership lists.\n *\n * @remarks\n * `userGroupMemberFilter`, `userGroupMemberSearch`, `userGroupMemberPath`, and\n * `groupIncludeSubGroups` are not supported. Use `userFilter` / `userPath` to\n * restrict which users are ingested, and `groupFilter` / `groupSearch` to\n * restrict which groups. Switch to `MicrosoftGraphOrgEntityProvider` if you\n * require any of these options.\n *\n * @public\n */\nexport class MicrosoftGraphIncrementalEntityProvider\n implements IncrementalEntityProvider<MSGraphCursor, MSGraphContext>\n{\n /**\n * Create one provider instance per provider entry in\n * `catalog.providers.microsoftGraphOrg`.\n */\n static fromConfig(\n configRoot: Config,\n options: MicrosoftGraphIncrementalEntityProviderOptions,\n ): MicrosoftGraphIncrementalEntityProvider[] {\n function getTransformer<T extends Function>(\n id: string,\n transformers?: T | Record<string, T>,\n ): T | undefined {\n if (['undefined', 'function'].includes(typeof transformers)) {\n return transformers as T;\n }\n return (transformers as Record<string, T>)[id];\n }\n\n return readProviderConfigs(configRoot).map(\n providerConfig =>\n new MicrosoftGraphIncrementalEntityProvider({\n id: providerConfig.id,\n provider: providerConfig,\n logger: options.logger,\n userTransformer: getTransformer(\n providerConfig.id,\n options.userTransformer,\n ),\n groupTransformer: getTransformer(\n providerConfig.id,\n options.groupTransformer,\n ),\n organizationTransformer: getTransformer(\n providerConfig.id,\n options.organizationTransformer,\n ),\n providerConfigTransformer: getTransformer(\n providerConfig.id,\n options.providerConfigTransformer,\n ),\n }),\n );\n }\n\n private readonly options: {\n id: string;\n provider: MicrosoftGraphProviderConfig;\n logger: LoggerService;\n userTransformer?: UserTransformer;\n groupTransformer?: GroupTransformer;\n organizationTransformer?: OrganizationTransformer;\n providerConfigTransformer?: ProviderConfigTransformer;\n };\n\n constructor(options: {\n id: string;\n provider: MicrosoftGraphProviderConfig;\n logger: LoggerService;\n userTransformer?: UserTransformer;\n groupTransformer?: GroupTransformer;\n organizationTransformer?: OrganizationTransformer;\n providerConfigTransformer?: ProviderConfigTransformer;\n }) {\n this.options = options;\n }\n\n /** {@inheritdoc @backstage/plugin-catalog-backend-module-incremental-ingestion#IncrementalEntityProvider.getProviderName} */\n getProviderName(): string {\n return `MicrosoftGraphIncrementalEntityProvider:${this.options.id}`;\n }\n\n /**\n * Sets up the Microsoft Graph client for the duration of a full ingestion\n * cycle. The optional `providerConfigTransformer` is applied here so that\n * dynamic config changes (e.g., rotating credentials) take effect at the\n * start of each cycle rather than mid-way through.\n */\n async around(\n burst: (context: MSGraphContext) => Promise<void>,\n ): Promise<void> {\n const provider = this.options.providerConfigTransformer\n ? await this.options.providerConfigTransformer(this.options.provider)\n : this.options.provider;\n\n if (\n provider.userGroupMemberFilter ||\n provider.userGroupMemberSearch ||\n provider.userGroupMemberPath\n ) {\n this.options.logger.warn(\n `${this.getProviderName()}: userGroupMemberFilter/Search/Path are not supported by ` +\n `MicrosoftGraphIncrementalEntityProvider. Users will be fetched via the standard ` +\n `userFilter/userPath options instead. Switch to MicrosoftGraphOrgEntityProvider if ` +\n `you require userGroupMember-based ingestion.`,\n );\n }\n\n if (provider.groupIncludeSubGroups) {\n this.options.logger.warn(\n `${this.getProviderName()}: groupIncludeSubGroups is not supported by ` +\n `MicrosoftGraphIncrementalEntityProvider and will be ignored. ` +\n `Switch to MicrosoftGraphOrgEntityProvider if you require this option.`,\n );\n }\n\n const client = MicrosoftGraphClient.create(provider);\n await burst({ client, provider });\n }\n\n /** {@inheritdoc @backstage/plugin-catalog-backend-module-incremental-ingestion#IncrementalEntityProvider.next} */\n async next(\n { client, provider }: MSGraphContext,\n cursor?: MSGraphCursor,\n ): Promise<EntityIteratorResult<MSGraphCursor>> {\n const phase = cursor?.phase ?? 'users';\n const nextLink = cursor?.nextLink;\n\n if (phase === 'users') {\n return this.readUsersPage(client, provider, nextLink);\n }\n return this.readGroupsPage(client, provider, nextLink);\n }\n\n private async readUsersPage(\n client: MicrosoftGraphClient,\n provider: MicrosoftGraphProviderConfig,\n nextLink: string | undefined,\n ): Promise<EntityIteratorResult<MSGraphCursor>> {\n const { items: rawUsers, nextLink: newNextLink } =\n await requestOnePage<MicrosoftGraph.User>(\n client,\n provider.userPath ?? 'users',\n {\n query: {\n filter: provider.userFilter,\n expand: provider.userExpand,\n select: provider.userSelect,\n top: USER_PAGE_SIZE,\n },\n queryMode: provider.queryMode,\n nextLink,\n },\n );\n\n const transformer = this.options.userTransformer ?? defaultUserTransformer;\n const limiter = limiterFactory(10);\n const entities: DeferredEntity[] = [];\n\n await Promise.all(\n rawUsers.map(user =>\n limiter(async () => {\n let userPhoto: string | undefined;\n if (user.id && provider.loadUserPhotos !== false) {\n try {\n userPhoto = await getUserPhotoGated(client, user.id, 120);\n } catch (e) {\n this.options.logger.debug(\n `${this.getProviderName()}: failed to load photo for user ${\n user.id\n }`,\n { error: e },\n );\n }\n }\n\n const entity = await transformer(user, userPhoto);\n if (entity) {\n entity.metadata.name = capEntityName(entity.metadata.name);\n entities.push({\n locationKey: `msgraph-org-provider:${this.options.id}`,\n entity: withLocations(this.options.id, entity),\n });\n }\n }),\n ),\n );\n\n this.options.logger.debug(\n `${this.getProviderName()}: read ${entities.length} users`,\n { phase: 'users', hasNextPage: !!newNextLink },\n );\n\n if (newNextLink) {\n return {\n done: false,\n entities,\n cursor: { phase: 'users', nextLink: newNextLink },\n };\n }\n\n return {\n done: false,\n entities,\n cursor: { phase: 'groups' },\n };\n }\n\n private async readGroupsPage(\n client: MicrosoftGraphClient,\n provider: MicrosoftGraphProviderConfig,\n nextLink: string | undefined,\n ): Promise<EntityIteratorResult<MSGraphCursor>> {\n const { items: rawGroups, nextLink: newNextLink } =\n await requestOnePage<MicrosoftGraph.Group>(\n client,\n provider.groupPath ?? 'groups',\n {\n query: {\n filter: provider.groupFilter,\n search: provider.groupSearch,\n expand: provider.groupExpand,\n select: provider.groupSelect,\n top: GROUP_PAGE_SIZE,\n },\n queryMode: provider.queryMode,\n nextLink,\n },\n );\n\n const groupTransformer =\n this.options.groupTransformer ?? defaultGroupTransformer;\n const userTransformer =\n this.options.userTransformer ?? defaultUserTransformer;\n const limiter = limiterFactory(10);\n const entities: DeferredEntity[] = [];\n\n // Emit the tenant root group on the very first groups page\n if (!nextLink) {\n try {\n const organization = await client.getOrganization(provider.tenantId);\n const orgTransformer =\n this.options.organizationTransformer ??\n defaultOrganizationTransformer;\n const rootGroup = await orgTransformer(organization);\n if (rootGroup) {\n entities.push({\n locationKey: `msgraph-org-provider:${this.options.id}`,\n entity: withLocations(this.options.id, rootGroup),\n });\n }\n } catch (e) {\n this.options.logger.warn(\n `${this.getProviderName()}: failed to read organization root group`,\n { error: e },\n );\n }\n }\n\n await Promise.all(\n rawGroups.map(group =>\n limiter(async () => {\n const entity = await groupTransformer(group);\n if (!entity) {\n return;\n }\n entity.metadata.name = capEntityName(entity.metadata.name);\n\n const userRefs: string[] = [];\n const childRefs: string[] = [];\n\n for await (const member of client.getGroupMembers(group.id!, {\n top: GROUP_PAGE_SIZE,\n // Request the minimum fields needed by defaultUserTransformer and\n // defaultGroupTransformer so member objects are never sparse.\n select: [\n 'id',\n 'displayName',\n 'mail',\n 'mailNickname',\n 'userPrincipalName',\n 'description',\n 'securityEnabled',\n ],\n })) {\n if (member['@odata.type'] === '#microsoft.graph.user') {\n try {\n const userEntity = await userTransformer(\n member as MicrosoftGraph.User,\n );\n if (userEntity) {\n userEntity.metadata.name = capEntityName(\n userEntity.metadata.name,\n );\n userRefs.push(stringifyEntityRef(userEntity));\n } else {\n this.options.logger.debug(\n `${this.getProviderName()}: group member user ${\n member.id\n } could not be transformed (sparse object?), skipping`,\n );\n }\n } catch (e) {\n this.options.logger.warn(\n `${this.getProviderName()}: group member user ${\n member.id\n } failed to transform, skipping`,\n { error: e },\n );\n }\n } else if (member['@odata.type'] === '#microsoft.graph.group') {\n // Only emit child refs when no group filter/search is active.\n // With a filter, child groups may not be ingested themselves,\n // which would produce dangling spec.children references.\n if (!provider.groupFilter && !provider.groupSearch) {\n try {\n const childEntity = await groupTransformer(\n member as MicrosoftGraph.Group,\n );\n if (childEntity) {\n childEntity.metadata.name = capEntityName(\n childEntity.metadata.name,\n );\n childRefs.push(stringifyEntityRef(childEntity));\n } else {\n this.options.logger.debug(\n `${this.getProviderName()}: group member child group ${\n member.id\n } could not be transformed (sparse object?), skipping`,\n );\n }\n } catch (e) {\n this.options.logger.warn(\n `${this.getProviderName()}: group member child group ${\n member.id\n } failed to transform, skipping`,\n { error: e },\n );\n }\n }\n }\n }\n\n // Merge fetched membership with any members/children the transformer\n // may have pre-populated, so custom transformers can augment the list.\n const existingMembers = Array.isArray(entity.spec?.members)\n ? (entity.spec.members as string[])\n : [];\n const existingChildren = Array.isArray(entity.spec?.children)\n ? (entity.spec.children as string[])\n : [];\n\n entities.push({\n locationKey: `msgraph-org-provider:${this.options.id}`,\n entity: withLocations(this.options.id, {\n ...entity,\n spec: {\n ...entity.spec,\n members: [...new Set([...existingMembers, ...userRefs])],\n children: [...new Set([...existingChildren, ...childRefs])],\n },\n }),\n });\n }),\n ),\n );\n\n this.options.logger.debug(\n `${this.getProviderName()}: read ${rawGroups.length} groups`,\n { phase: 'groups', hasNextPage: !!newNextLink },\n );\n\n if (newNextLink) {\n return {\n done: false,\n entities,\n cursor: { phase: 'groups', nextLink: newNextLink },\n };\n }\n\n return { done: true, entities };\n }\n}\n"],"names":["crypto","MICROSOFT_GRAPH_USER_ID_ANNOTATION","MICROSOFT_GRAPH_GROUP_ID_ANNOTATION","MICROSOFT_GRAPH_TENANT_ID_ANNOTATION","ANNOTATION_LOCATION","ANNOTATION_ORIGIN_LOCATION","readProviderConfigs","MicrosoftGraphClient","requestOnePage","defaultUserTransformer","limiterFactory","getUserPhotoGated","defaultGroupTransformer","defaultOrganizationTransformer","stringifyEntityRef"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"alpha.cjs.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;"}
|
package/dist/alpha.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
async function requestOnePage(client, path, options = {}) {
|
|
4
|
+
const { query, queryMode, nextLink, signal } = options;
|
|
5
|
+
const appliedQueryMode = query?.search ? "advanced" : queryMode ?? "basic";
|
|
6
|
+
const finalQuery = appliedQueryMode === "advanced" ? { ...query ?? {}, count: true } : query;
|
|
7
|
+
const headers = appliedQueryMode === "advanced" ? { ConsistencyLevel: "eventual" } : {};
|
|
8
|
+
const response = nextLink ? await client.requestRaw(nextLink, headers, 2, signal) : await client.requestApi(path, finalQuery, headers, signal);
|
|
9
|
+
if (response.status !== 200) {
|
|
10
|
+
let message = `HTTP ${response.status}`;
|
|
11
|
+
try {
|
|
12
|
+
const body = await response.json();
|
|
13
|
+
const err = body?.error;
|
|
14
|
+
if (err?.code || err?.message) {
|
|
15
|
+
message = `${err.code} - ${err.message}`;
|
|
16
|
+
}
|
|
17
|
+
} catch {
|
|
18
|
+
}
|
|
19
|
+
throw new Error(
|
|
20
|
+
`Error while reading ${nextLink ?? path} from Microsoft Graph: ${message}`
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
const result = await response.json();
|
|
24
|
+
return {
|
|
25
|
+
items: result.value,
|
|
26
|
+
nextLink: result["@odata.nextLink"]
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
async function getUserPhotoGated(client, userId, maxSize) {
|
|
30
|
+
const check = await client.requestApi(`users/${userId}/photo`);
|
|
31
|
+
if (check.status === 404) return void 0;
|
|
32
|
+
if (check.status !== 200) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`Unexpected status ${check.status} when checking photo for user ${userId}`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
return await client.getUserPhotoWithSizeLimit(userId, maxSize);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
exports.getUserPhotoGated = getUserPhotoGated;
|
|
41
|
+
exports.requestOnePage = requestOnePage;
|
|
42
|
+
//# sourceMappingURL=clientHelpers.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clientHelpers.cjs.js","sources":["../src/clientHelpers.ts"],"sourcesContent":["/*\n * Copyright 2026 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n MicrosoftGraphClient,\n ODataQuery,\n} from '@backstage/plugin-catalog-backend-module-msgraph';\n\n/**\n * Fetches a single page of Graph API results.\n *\n * When `options.nextLink` is provided it is followed directly (all query\n * parameters are already encoded in it). Otherwise the request is built from\n * `options.query`.\n *\n * MS Graph requires `ConsistencyLevel: eventual` + `$count=true` for advanced\n * queries using `ne`/`not` operators in `$filter` or using `$search`.\n */\nexport async function requestOnePage<T>(\n client: MicrosoftGraphClient,\n path: string,\n options: {\n query?: ODataQuery;\n queryMode?: 'basic' | 'advanced';\n nextLink?: string;\n signal?: AbortSignal;\n } = {},\n): Promise<{ items: T[]; nextLink?: string }> {\n const { query, queryMode, nextLink, signal } = options;\n const appliedQueryMode = query?.search ? 'advanced' : queryMode ?? 'basic';\n\n // Microsoft Graph requires $count=true whenever ConsistencyLevel: eventual is set,\n // including plain listing requests with no $filter or $search.\n const finalQuery =\n appliedQueryMode === 'advanced' ? { ...(query ?? {}), count: true } : query;\n\n const headers: Record<string, string> =\n appliedQueryMode === 'advanced' ? { ConsistencyLevel: 'eventual' } : {};\n\n const response = nextLink\n ? await client.requestRaw(nextLink, headers, 2, signal)\n : await client.requestApi(path, finalQuery, headers, signal);\n\n if (response.status !== 200) {\n let message = `HTTP ${response.status}`;\n try {\n const body = await response.json();\n const err = body?.error;\n if (err?.code || err?.message) {\n message = `${err.code} - ${err.message}`;\n }\n } catch {\n // Response body is not JSON; fall back to HTTP status above\n }\n throw new Error(\n `Error while reading ${\n nextLink ?? path\n } from Microsoft Graph: ${message}`,\n );\n }\n\n const result = await response.json();\n return {\n items: result.value as T[],\n nextLink: result['@odata.nextLink'],\n };\n}\n\n/**\n * Like `getUserPhotoWithSizeLimit` but skips the size-listing call for users\n * with no photo. For users without a photo: 1 fast check call. For users with\n * a photo: 1 check + the normal size-limited fetch (2 more calls).\n *\n * Returns `undefined` only for 404 (no photo assigned). Throws for any other\n * non-200 status so callers can distinguish \"no photo\" from real errors.\n */\nexport async function getUserPhotoGated(\n client: MicrosoftGraphClient,\n userId: string,\n maxSize: number,\n): Promise<string | undefined> {\n const check = await client.requestApi(`users/${userId}/photo`);\n if (check.status === 404) return undefined;\n if (check.status !== 200) {\n throw new Error(\n `Unexpected status ${check.status} when checking photo for user ${userId}`,\n );\n }\n return await client.getUserPhotoWithSizeLimit(userId, maxSize);\n}\n"],"names":[],"mappings":";;AA+BA,eAAsB,cAAA,CACpB,MAAA,EACA,IAAA,EACA,OAAA,GAKI,EAAC,EACuC;AAC5C,EAAA,MAAM,EAAE,KAAA,EAAO,SAAA,EAAW,QAAA,EAAU,QAAO,GAAI,OAAA;AAC/C,EAAA,MAAM,gBAAA,GAAmB,KAAA,EAAO,MAAA,GAAS,UAAA,GAAa,SAAA,IAAa,OAAA;AAInE,EAAA,MAAM,UAAA,GACJ,gBAAA,KAAqB,UAAA,GAAa,EAAE,GAAI,SAAS,EAAC,EAAI,KAAA,EAAO,IAAA,EAAK,GAAI,KAAA;AAExE,EAAA,MAAM,UACJ,gBAAA,KAAqB,UAAA,GAAa,EAAE,gBAAA,EAAkB,UAAA,KAAe,EAAC;AAExE,EAAA,MAAM,WAAW,QAAA,GACb,MAAM,MAAA,CAAO,UAAA,CAAW,UAAU,OAAA,EAAS,CAAA,EAAG,MAAM,CAAA,GACpD,MAAM,MAAA,CAAO,UAAA,CAAW,IAAA,EAAM,UAAA,EAAY,SAAS,MAAM,CAAA;AAE7D,EAAA,IAAI,QAAA,CAAS,WAAW,GAAA,EAAK;AAC3B,IAAA,IAAI,OAAA,GAAU,CAAA,KAAA,EAAQ,QAAA,CAAS,MAAM,CAAA,CAAA;AACrC,IAAA,IAAI;AACF,MAAA,MAAM,IAAA,GAAO,MAAM,QAAA,CAAS,IAAA,EAAK;AACjC,MAAA,MAAM,MAAM,IAAA,EAAM,KAAA;AAClB,MAAA,IAAI,GAAA,EAAK,IAAA,IAAQ,GAAA,EAAK,OAAA,EAAS;AAC7B,QAAA,OAAA,GAAU,CAAA,EAAG,GAAA,CAAI,IAAI,CAAA,GAAA,EAAM,IAAI,OAAO,CAAA,CAAA;AAAA,MACxC;AAAA,IACF,CAAA,CAAA,MAAQ;AAAA,IAER;AACA,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,oBAAA,EACE,QAAA,IAAY,IACd,CAAA,uBAAA,EAA0B,OAAO,CAAA;AAAA,KACnC;AAAA,EACF;AAEA,EAAA,MAAM,MAAA,GAAS,MAAM,QAAA,CAAS,IAAA,EAAK;AACnC,EAAA,OAAO;AAAA,IACL,OAAO,MAAA,CAAO,KAAA;AAAA,IACd,QAAA,EAAU,OAAO,iBAAiB;AAAA,GACpC;AACF;AAUA,eAAsB,iBAAA,CACpB,MAAA,EACA,MAAA,EACA,OAAA,EAC6B;AAC7B,EAAA,MAAM,QAAQ,MAAM,MAAA,CAAO,UAAA,CAAW,CAAA,MAAA,EAAS,MAAM,CAAA,MAAA,CAAQ,CAAA;AAC7D,EAAA,IAAI,KAAA,CAAM,MAAA,KAAW,GAAA,EAAK,OAAO,MAAA;AACjC,EAAA,IAAI,KAAA,CAAM,WAAW,GAAA,EAAK;AACxB,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,kBAAA,EAAqB,KAAA,CAAM,MAAM,CAAA,8BAAA,EAAiC,MAAM,CAAA;AAAA,KAC1E;AAAA,EACF;AACA,EAAA,OAAO,MAAM,MAAA,CAAO,yBAAA,CAA0B,MAAA,EAAQ,OAAO,CAAA;AAC/D;;;;;"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var catalogModuleMicrosoftGraphIncrementalEntityProvider = require('./module/catalogModuleMicrosoftGraphIncrementalEntityProvider.cjs.js');
|
|
6
|
+
var MicrosoftGraphIncrementalEntityProvider = require('./MicrosoftGraphIncrementalEntityProvider.cjs.js');
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
exports.catalogModuleMicrosoftGraphIncrementalEntityProvider = catalogModuleMicrosoftGraphIncrementalEntityProvider.catalogModuleMicrosoftGraphIncrementalEntityProvider;
|
|
11
|
+
exports.default = catalogModuleMicrosoftGraphIncrementalEntityProvider.catalogModuleMicrosoftGraphIncrementalEntityProvider;
|
|
12
|
+
exports.microsoftGraphIncrementalEntityProviderTransformExtensionPoint = catalogModuleMicrosoftGraphIncrementalEntityProvider.microsoftGraphIncrementalEntityProviderTransformExtensionPoint;
|
|
13
|
+
exports.MicrosoftGraphIncrementalEntityProvider = MicrosoftGraphIncrementalEntityProvider.MicrosoftGraphIncrementalEntityProvider;
|
|
14
|
+
//# sourceMappingURL=index.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import * as _backstage_backend_plugin_api from '@backstage/backend-plugin-api';
|
|
2
|
+
import { LoggerService } from '@backstage/backend-plugin-api';
|
|
3
|
+
import { UserTransformer, GroupTransformer, OrganizationTransformer, ProviderConfigTransformer, MicrosoftGraphClient, MicrosoftGraphProviderConfig } from '@backstage/plugin-catalog-backend-module-msgraph';
|
|
4
|
+
import { Config } from '@backstage/config';
|
|
5
|
+
import { IncrementalEntityProvider, EntityIteratorResult } from '@backstage/plugin-catalog-backend-module-incremental-ingestion';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Interface for
|
|
9
|
+
* {@link microsoftGraphIncrementalEntityProviderTransformExtensionPoint}.
|
|
10
|
+
*
|
|
11
|
+
* @public
|
|
12
|
+
*/
|
|
13
|
+
interface MicrosoftGraphIncrementalEntityProviderTransformsExtensionPoint {
|
|
14
|
+
/**
|
|
15
|
+
* Set the function that transforms a user entry in msgraph to an entity.
|
|
16
|
+
* Optionally, you can pass separate transformers per provider ID.
|
|
17
|
+
*/
|
|
18
|
+
setUserTransformer(transformer: UserTransformer | Record<string, UserTransformer>): void;
|
|
19
|
+
/**
|
|
20
|
+
* Set the function that transforms a group entry in msgraph to an entity.
|
|
21
|
+
* Optionally, you can pass separate transformers per provider ID.
|
|
22
|
+
*/
|
|
23
|
+
setGroupTransformer(transformer: GroupTransformer | Record<string, GroupTransformer>): void;
|
|
24
|
+
/**
|
|
25
|
+
* Set the function that transforms an organization entry in msgraph to an
|
|
26
|
+
* entity. Optionally, you can pass separate transformers per provider ID.
|
|
27
|
+
*/
|
|
28
|
+
setOrganizationTransformer(transformer: OrganizationTransformer | Record<string, OrganizationTransformer>): void;
|
|
29
|
+
/**
|
|
30
|
+
* Set the function that transforms provider config dynamically.
|
|
31
|
+
* Optionally, you can pass separate transformers per provider ID.
|
|
32
|
+
*/
|
|
33
|
+
setProviderConfigTransformer(transformer: ProviderConfigTransformer | Record<string, ProviderConfigTransformer>): void;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Extension point used to customize the transforms applied by the incremental
|
|
37
|
+
* module.
|
|
38
|
+
*
|
|
39
|
+
* @public
|
|
40
|
+
*/
|
|
41
|
+
declare const microsoftGraphIncrementalEntityProviderTransformExtensionPoint: _backstage_backend_plugin_api.ExtensionPoint<MicrosoftGraphIncrementalEntityProviderTransformsExtensionPoint>;
|
|
42
|
+
/**
|
|
43
|
+
* Registers {@link MicrosoftGraphIncrementalEntityProvider} instances with the
|
|
44
|
+
* catalog's incremental ingestion extension point.
|
|
45
|
+
*
|
|
46
|
+
* This module requires `catalogModuleIncrementalIngestionEntityProvider` to
|
|
47
|
+
* also be installed in the backend.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```ts
|
|
51
|
+
* // packages/backend/src/index.ts
|
|
52
|
+
* backend.add(import('@backstage/plugin-catalog-backend-module-incremental-ingestion'));
|
|
53
|
+
* backend.add(import('@backstage/plugin-catalog-backend-module-msgraph-incremental'));
|
|
54
|
+
* ```
|
|
55
|
+
*
|
|
56
|
+
* @public
|
|
57
|
+
*/
|
|
58
|
+
declare const catalogModuleMicrosoftGraphIncrementalEntityProvider: _backstage_backend_plugin_api.BackendFeature;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Pagination cursor used by {@link MicrosoftGraphIncrementalEntityProvider}.
|
|
62
|
+
*
|
|
63
|
+
* The `nextLink` field holds the `@odata.nextLink` URL returned by the
|
|
64
|
+
* Microsoft Graph API, which encodes all state needed to resume a paged
|
|
65
|
+
* request. An absent value means the current phase is starting fresh.
|
|
66
|
+
*
|
|
67
|
+
* @public
|
|
68
|
+
*/
|
|
69
|
+
type MSGraphCursor = {
|
|
70
|
+
phase: 'users' | 'groups';
|
|
71
|
+
nextLink?: string;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Context passed to each burst of {@link MicrosoftGraphIncrementalEntityProvider}.
|
|
75
|
+
*
|
|
76
|
+
* @public
|
|
77
|
+
*/
|
|
78
|
+
type MSGraphContext = {
|
|
79
|
+
client: MicrosoftGraphClient;
|
|
80
|
+
provider: MicrosoftGraphProviderConfig;
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Options for {@link MicrosoftGraphIncrementalEntityProvider}.
|
|
84
|
+
*
|
|
85
|
+
* @public
|
|
86
|
+
*/
|
|
87
|
+
interface MicrosoftGraphIncrementalEntityProviderOptions {
|
|
88
|
+
/**
|
|
89
|
+
* The logger to use.
|
|
90
|
+
*/
|
|
91
|
+
logger: LoggerService;
|
|
92
|
+
/**
|
|
93
|
+
* The function that transforms a user entry in msgraph to an entity.
|
|
94
|
+
* Optionally, you can pass separate transformers per provider ID.
|
|
95
|
+
*/
|
|
96
|
+
userTransformer?: UserTransformer | Record<string, UserTransformer>;
|
|
97
|
+
/**
|
|
98
|
+
* The function that transforms a group entry in msgraph to an entity.
|
|
99
|
+
* Optionally, you can pass separate transformers per provider ID.
|
|
100
|
+
*/
|
|
101
|
+
groupTransformer?: GroupTransformer | Record<string, GroupTransformer>;
|
|
102
|
+
/**
|
|
103
|
+
* The function that transforms an organization entry in msgraph to an entity.
|
|
104
|
+
* Optionally, you can pass separate transformers per provider ID.
|
|
105
|
+
*/
|
|
106
|
+
organizationTransformer?: OrganizationTransformer | Record<string, OrganizationTransformer>;
|
|
107
|
+
/**
|
|
108
|
+
* The function that transforms provider config dynamically before each sync.
|
|
109
|
+
* Optionally, you can pass separate transformers per provider ID.
|
|
110
|
+
*/
|
|
111
|
+
providerConfigTransformer?: ProviderConfigTransformer | Record<string, ProviderConfigTransformer>;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Incrementally reads user and group entries out of Microsoft Graph, one page
|
|
115
|
+
* at a time, and provides them as User and Group entities for the catalog.
|
|
116
|
+
*
|
|
117
|
+
* Unlike `MicrosoftGraphOrgEntityProvider`, this provider never holds the full
|
|
118
|
+
* dataset in memory at once. Each burst processes a single page (up to 999
|
|
119
|
+
* users or 100 groups). This makes it suitable for very large tenants and
|
|
120
|
+
* avoids the memory pressure and long-running task issues of the full-scan
|
|
121
|
+
* provider.
|
|
122
|
+
*
|
|
123
|
+
* The Microsoft Graph `@odata.nextLink` URL is stored as the cursor, so a pod
|
|
124
|
+
* restart during ingestion resumes from the last completed page.
|
|
125
|
+
*
|
|
126
|
+
* Group membership (`spec.members`) is resolved inline during the groups phase
|
|
127
|
+
* by fetching the direct members of each group. The catalog's built-in relation
|
|
128
|
+
* stitching derives `spec.memberOf` on users from these group membership lists.
|
|
129
|
+
*
|
|
130
|
+
* @remarks
|
|
131
|
+
* `userGroupMemberFilter`, `userGroupMemberSearch`, `userGroupMemberPath`, and
|
|
132
|
+
* `groupIncludeSubGroups` are not supported. Use `userFilter` / `userPath` to
|
|
133
|
+
* restrict which users are ingested, and `groupFilter` / `groupSearch` to
|
|
134
|
+
* restrict which groups. Switch to `MicrosoftGraphOrgEntityProvider` if you
|
|
135
|
+
* require any of these options.
|
|
136
|
+
*
|
|
137
|
+
* @public
|
|
138
|
+
*/
|
|
139
|
+
declare class MicrosoftGraphIncrementalEntityProvider implements IncrementalEntityProvider<MSGraphCursor, MSGraphContext> {
|
|
140
|
+
/**
|
|
141
|
+
* Create one provider instance per provider entry in
|
|
142
|
+
* `catalog.providers.microsoftGraphOrg`.
|
|
143
|
+
*/
|
|
144
|
+
static fromConfig(configRoot: Config, options: MicrosoftGraphIncrementalEntityProviderOptions): MicrosoftGraphIncrementalEntityProvider[];
|
|
145
|
+
private readonly options;
|
|
146
|
+
constructor(options: {
|
|
147
|
+
id: string;
|
|
148
|
+
provider: MicrosoftGraphProviderConfig;
|
|
149
|
+
logger: LoggerService;
|
|
150
|
+
userTransformer?: UserTransformer;
|
|
151
|
+
groupTransformer?: GroupTransformer;
|
|
152
|
+
organizationTransformer?: OrganizationTransformer;
|
|
153
|
+
providerConfigTransformer?: ProviderConfigTransformer;
|
|
154
|
+
});
|
|
155
|
+
/** {@inheritdoc @backstage/plugin-catalog-backend-module-incremental-ingestion#IncrementalEntityProvider.getProviderName} */
|
|
156
|
+
getProviderName(): string;
|
|
157
|
+
/**
|
|
158
|
+
* Sets up the Microsoft Graph client for the duration of a full ingestion
|
|
159
|
+
* cycle. The optional `providerConfigTransformer` is applied here so that
|
|
160
|
+
* dynamic config changes (e.g., rotating credentials) take effect at the
|
|
161
|
+
* start of each cycle rather than mid-way through.
|
|
162
|
+
*/
|
|
163
|
+
around(burst: (context: MSGraphContext) => Promise<void>): Promise<void>;
|
|
164
|
+
/** {@inheritdoc @backstage/plugin-catalog-backend-module-incremental-ingestion#IncrementalEntityProvider.next} */
|
|
165
|
+
next({ client, provider }: MSGraphContext, cursor?: MSGraphCursor): Promise<EntityIteratorResult<MSGraphCursor>>;
|
|
166
|
+
private readUsersPage;
|
|
167
|
+
private readGroupsPage;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export { MicrosoftGraphIncrementalEntityProvider, catalogModuleMicrosoftGraphIncrementalEntityProvider, catalogModuleMicrosoftGraphIncrementalEntityProvider as default, microsoftGraphIncrementalEntityProviderTransformExtensionPoint };
|
|
171
|
+
export type { MSGraphContext, MSGraphCursor, MicrosoftGraphIncrementalEntityProviderOptions, MicrosoftGraphIncrementalEntityProviderTransformsExtensionPoint };
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var backendPluginApi = require('@backstage/backend-plugin-api');
|
|
4
|
+
var pluginCatalogBackendModuleIncrementalIngestion = require('@backstage/plugin-catalog-backend-module-incremental-ingestion');
|
|
5
|
+
var pluginCatalogBackendModuleMsgraph = require('@backstage/plugin-catalog-backend-module-msgraph');
|
|
6
|
+
var MicrosoftGraphIncrementalEntityProvider = require('../MicrosoftGraphIncrementalEntityProvider.cjs.js');
|
|
7
|
+
|
|
8
|
+
const microsoftGraphIncrementalEntityProviderTransformExtensionPoint = backendPluginApi.createExtensionPoint(
|
|
9
|
+
{
|
|
10
|
+
id: "catalog.microsoftGraphIncrementalEntityProvider.transforms"
|
|
11
|
+
}
|
|
12
|
+
);
|
|
13
|
+
const catalogModuleMicrosoftGraphIncrementalEntityProvider = backendPluginApi.createBackendModule({
|
|
14
|
+
pluginId: "catalog",
|
|
15
|
+
moduleId: "microsoftGraphIncrementalEntityProvider",
|
|
16
|
+
register(env) {
|
|
17
|
+
let userTransformer;
|
|
18
|
+
let groupTransformer;
|
|
19
|
+
let organizationTransformer;
|
|
20
|
+
let providerConfigTransformer;
|
|
21
|
+
env.registerExtensionPoint(
|
|
22
|
+
microsoftGraphIncrementalEntityProviderTransformExtensionPoint,
|
|
23
|
+
{
|
|
24
|
+
setUserTransformer(transformer) {
|
|
25
|
+
if (userTransformer) {
|
|
26
|
+
throw new Error("User transformer may only be set once");
|
|
27
|
+
}
|
|
28
|
+
userTransformer = transformer;
|
|
29
|
+
},
|
|
30
|
+
setGroupTransformer(transformer) {
|
|
31
|
+
if (groupTransformer) {
|
|
32
|
+
throw new Error("Group transformer may only be set once");
|
|
33
|
+
}
|
|
34
|
+
groupTransformer = transformer;
|
|
35
|
+
},
|
|
36
|
+
setOrganizationTransformer(transformer) {
|
|
37
|
+
if (organizationTransformer) {
|
|
38
|
+
throw new Error("Organization transformer may only be set once");
|
|
39
|
+
}
|
|
40
|
+
organizationTransformer = transformer;
|
|
41
|
+
},
|
|
42
|
+
setProviderConfigTransformer(transformer) {
|
|
43
|
+
if (providerConfigTransformer) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
"Provider config transformer may only be set once"
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
providerConfigTransformer = transformer;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
env.registerInit({
|
|
53
|
+
deps: {
|
|
54
|
+
config: backendPluginApi.coreServices.rootConfig,
|
|
55
|
+
logger: backendPluginApi.coreServices.logger,
|
|
56
|
+
incremental: pluginCatalogBackendModuleIncrementalIngestion.incrementalIngestionProvidersExtensionPoint
|
|
57
|
+
},
|
|
58
|
+
async init({ config, logger, incremental }) {
|
|
59
|
+
const providerConfigs = pluginCatalogBackendModuleMsgraph.readProviderConfigs(config);
|
|
60
|
+
for (const providerConfig of providerConfigs) {
|
|
61
|
+
const provider = new MicrosoftGraphIncrementalEntityProvider.MicrosoftGraphIncrementalEntityProvider({
|
|
62
|
+
id: providerConfig.id,
|
|
63
|
+
provider: providerConfig,
|
|
64
|
+
logger,
|
|
65
|
+
userTransformer: resolveTransformer(
|
|
66
|
+
providerConfig.id,
|
|
67
|
+
userTransformer
|
|
68
|
+
),
|
|
69
|
+
groupTransformer: resolveTransformer(
|
|
70
|
+
providerConfig.id,
|
|
71
|
+
groupTransformer
|
|
72
|
+
),
|
|
73
|
+
organizationTransformer: resolveTransformer(
|
|
74
|
+
providerConfig.id,
|
|
75
|
+
organizationTransformer
|
|
76
|
+
),
|
|
77
|
+
providerConfigTransformer: resolveTransformer(
|
|
78
|
+
providerConfig.id,
|
|
79
|
+
providerConfigTransformer
|
|
80
|
+
)
|
|
81
|
+
});
|
|
82
|
+
const restLength = deriveRestLength(providerConfig, logger);
|
|
83
|
+
incremental.addProvider({
|
|
84
|
+
provider,
|
|
85
|
+
options: {
|
|
86
|
+
burstInterval: { seconds: 3 },
|
|
87
|
+
burstLength: { minutes: 5 },
|
|
88
|
+
restLength,
|
|
89
|
+
backoff: [
|
|
90
|
+
{ seconds: 30 },
|
|
91
|
+
{ minutes: 3 },
|
|
92
|
+
{ minutes: 30 },
|
|
93
|
+
{ hours: 3 }
|
|
94
|
+
]
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
function resolveTransformer(id, transformer) {
|
|
103
|
+
if (["undefined", "function"].includes(typeof transformer)) {
|
|
104
|
+
return transformer;
|
|
105
|
+
}
|
|
106
|
+
return transformer[id];
|
|
107
|
+
}
|
|
108
|
+
function deriveRestLength(providerConfig, logger) {
|
|
109
|
+
const freq = providerConfig.schedule?.frequency;
|
|
110
|
+
if (freq && typeof freq === "object" && !("cron" in freq) && !("trigger" in freq)) {
|
|
111
|
+
return freq;
|
|
112
|
+
}
|
|
113
|
+
if (freq) {
|
|
114
|
+
logger.warn(
|
|
115
|
+
`MicrosoftGraphIncrementalEntityProvider:${providerConfig.id}: schedule.frequency is not a duration-based schedule; cannot derive restLength from it. Defaulting restLength to 8 hours.`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return { hours: 8 };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
exports.catalogModuleMicrosoftGraphIncrementalEntityProvider = catalogModuleMicrosoftGraphIncrementalEntityProvider;
|
|
122
|
+
exports.microsoftGraphIncrementalEntityProviderTransformExtensionPoint = microsoftGraphIncrementalEntityProviderTransformExtensionPoint;
|
|
123
|
+
//# sourceMappingURL=catalogModuleMicrosoftGraphIncrementalEntityProvider.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalogModuleMicrosoftGraphIncrementalEntityProvider.cjs.js","sources":["../../src/module/catalogModuleMicrosoftGraphIncrementalEntityProvider.ts"],"sourcesContent":["/*\n * Copyright 2026 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n coreServices,\n createBackendModule,\n createExtensionPoint,\n LoggerService,\n} from '@backstage/backend-plugin-api';\nimport { incrementalIngestionProvidersExtensionPoint } from '@backstage/plugin-catalog-backend-module-incremental-ingestion';\nimport {\n GroupTransformer,\n OrganizationTransformer,\n ProviderConfigTransformer,\n UserTransformer,\n readProviderConfigs,\n} from '@backstage/plugin-catalog-backend-module-msgraph';\nimport { HumanDuration } from '@backstage/types';\nimport {\n MicrosoftGraphIncrementalEntityProvider,\n MSGraphContext,\n MSGraphCursor,\n} from '../MicrosoftGraphIncrementalEntityProvider';\n\n/**\n * Interface for\n * {@link microsoftGraphIncrementalEntityProviderTransformExtensionPoint}.\n *\n * @public\n */\nexport interface MicrosoftGraphIncrementalEntityProviderTransformsExtensionPoint {\n /**\n * Set the function that transforms a user entry in msgraph to an entity.\n * Optionally, you can pass separate transformers per provider ID.\n */\n setUserTransformer(\n transformer: UserTransformer | Record<string, UserTransformer>,\n ): void;\n\n /**\n * Set the function that transforms a group entry in msgraph to an entity.\n * Optionally, you can pass separate transformers per provider ID.\n */\n setGroupTransformer(\n transformer: GroupTransformer | Record<string, GroupTransformer>,\n ): void;\n\n /**\n * Set the function that transforms an organization entry in msgraph to an\n * entity. Optionally, you can pass separate transformers per provider ID.\n */\n setOrganizationTransformer(\n transformer:\n | OrganizationTransformer\n | Record<string, OrganizationTransformer>,\n ): void;\n\n /**\n * Set the function that transforms provider config dynamically.\n * Optionally, you can pass separate transformers per provider ID.\n */\n setProviderConfigTransformer(\n transformer:\n | ProviderConfigTransformer\n | Record<string, ProviderConfigTransformer>,\n ): void;\n}\n\n/**\n * Extension point used to customize the transforms applied by the incremental\n * module.\n *\n * @public\n */\nexport const microsoftGraphIncrementalEntityProviderTransformExtensionPoint =\n createExtensionPoint<MicrosoftGraphIncrementalEntityProviderTransformsExtensionPoint>(\n {\n id: 'catalog.microsoftGraphIncrementalEntityProvider.transforms',\n },\n );\n\n/**\n * Registers {@link MicrosoftGraphIncrementalEntityProvider} instances with the\n * catalog's incremental ingestion extension point.\n *\n * This module requires `catalogModuleIncrementalIngestionEntityProvider` to\n * also be installed in the backend.\n *\n * @example\n * ```ts\n * // packages/backend/src/index.ts\n * backend.add(import('@backstage/plugin-catalog-backend-module-incremental-ingestion'));\n * backend.add(import('@backstage/plugin-catalog-backend-module-msgraph-incremental'));\n * ```\n *\n * @public\n */\nexport const catalogModuleMicrosoftGraphIncrementalEntityProvider =\n createBackendModule({\n pluginId: 'catalog',\n moduleId: 'microsoftGraphIncrementalEntityProvider',\n register(env) {\n let userTransformer:\n | UserTransformer\n | Record<string, UserTransformer>\n | undefined;\n let groupTransformer:\n | GroupTransformer\n | Record<string, GroupTransformer>\n | undefined;\n let organizationTransformer:\n | OrganizationTransformer\n | Record<string, OrganizationTransformer>\n | undefined;\n let providerConfigTransformer:\n | ProviderConfigTransformer\n | Record<string, ProviderConfigTransformer>\n | undefined;\n\n env.registerExtensionPoint(\n microsoftGraphIncrementalEntityProviderTransformExtensionPoint,\n {\n setUserTransformer(transformer) {\n if (userTransformer) {\n throw new Error('User transformer may only be set once');\n }\n userTransformer = transformer;\n },\n setGroupTransformer(transformer) {\n if (groupTransformer) {\n throw new Error('Group transformer may only be set once');\n }\n groupTransformer = transformer;\n },\n setOrganizationTransformer(transformer) {\n if (organizationTransformer) {\n throw new Error('Organization transformer may only be set once');\n }\n organizationTransformer = transformer;\n },\n setProviderConfigTransformer(transformer) {\n if (providerConfigTransformer) {\n throw new Error(\n 'Provider config transformer may only be set once',\n );\n }\n providerConfigTransformer = transformer;\n },\n },\n );\n\n env.registerInit({\n deps: {\n config: coreServices.rootConfig,\n logger: coreServices.logger,\n incremental: incrementalIngestionProvidersExtensionPoint,\n },\n async init({ config, logger, incremental }) {\n const providerConfigs = readProviderConfigs(config);\n\n for (const providerConfig of providerConfigs) {\n const provider = new MicrosoftGraphIncrementalEntityProvider({\n id: providerConfig.id,\n provider: providerConfig,\n logger,\n userTransformer: resolveTransformer(\n providerConfig.id,\n userTransformer,\n ),\n groupTransformer: resolveTransformer(\n providerConfig.id,\n groupTransformer,\n ),\n organizationTransformer: resolveTransformer(\n providerConfig.id,\n organizationTransformer,\n ),\n providerConfigTransformer: resolveTransformer(\n providerConfig.id,\n providerConfigTransformer,\n ),\n });\n\n const restLength = deriveRestLength(providerConfig, logger);\n\n incremental.addProvider<MSGraphCursor, MSGraphContext>({\n provider,\n options: {\n burstInterval: { seconds: 3 },\n burstLength: { minutes: 5 },\n restLength,\n backoff: [\n { seconds: 30 },\n { minutes: 3 },\n { minutes: 30 },\n { hours: 3 },\n ],\n },\n });\n }\n },\n });\n },\n });\n\nfunction resolveTransformer<T extends Function>(\n id: string,\n transformer?: T | Record<string, T>,\n): T | undefined {\n if (['undefined', 'function'].includes(typeof transformer)) {\n return transformer as T;\n }\n return (transformer as Record<string, T>)[id];\n}\n\nfunction deriveRestLength(\n providerConfig: ReturnType<typeof readProviderConfigs>[number],\n logger: LoggerService,\n): HumanDuration {\n const freq = providerConfig.schedule?.frequency;\n // Only treat plain duration objects as restLength — exclude cron expressions\n // and any other non-duration schedule types (e.g. manual triggers).\n if (\n freq &&\n typeof freq === 'object' &&\n !('cron' in freq) &&\n !('trigger' in freq)\n ) {\n return freq as HumanDuration;\n }\n if (freq) {\n logger.warn(\n `MicrosoftGraphIncrementalEntityProvider:${providerConfig.id}: ` +\n `schedule.frequency is not a duration-based schedule; cannot derive restLength from it. ` +\n `Defaulting restLength to 8 hours.`,\n );\n }\n return { hours: 8 };\n}\n"],"names":["createExtensionPoint","createBackendModule","coreServices","incrementalIngestionProvidersExtensionPoint","readProviderConfigs","MicrosoftGraphIncrementalEntityProvider"],"mappings":";;;;;;;AAuFO,MAAM,8DAAA,GACXA,qCAAA;AAAA,EACE;AAAA,IACE,EAAA,EAAI;AAAA;AAER;AAkBK,MAAM,uDACXC,oCAAA,CAAoB;AAAA,EAClB,QAAA,EAAU,SAAA;AAAA,EACV,QAAA,EAAU,yCAAA;AAAA,EACV,SAAS,GAAA,EAAK;AACZ,IAAA,IAAI,eAAA;AAIJ,IAAA,IAAI,gBAAA;AAIJ,IAAA,IAAI,uBAAA;AAIJ,IAAA,IAAI,yBAAA;AAKJ,IAAA,GAAA,CAAI,sBAAA;AAAA,MACF,8DAAA;AAAA,MACA;AAAA,QACE,mBAAmB,WAAA,EAAa;AAC9B,UAAA,IAAI,eAAA,EAAiB;AACnB,YAAA,MAAM,IAAI,MAAM,uCAAuC,CAAA;AAAA,UACzD;AACA,UAAA,eAAA,GAAkB,WAAA;AAAA,QACpB,CAAA;AAAA,QACA,oBAAoB,WAAA,EAAa;AAC/B,UAAA,IAAI,gBAAA,EAAkB;AACpB,YAAA,MAAM,IAAI,MAAM,wCAAwC,CAAA;AAAA,UAC1D;AACA,UAAA,gBAAA,GAAmB,WAAA;AAAA,QACrB,CAAA;AAAA,QACA,2BAA2B,WAAA,EAAa;AACtC,UAAA,IAAI,uBAAA,EAAyB;AAC3B,YAAA,MAAM,IAAI,MAAM,+CAA+C,CAAA;AAAA,UACjE;AACA,UAAA,uBAAA,GAA0B,WAAA;AAAA,QAC5B,CAAA;AAAA,QACA,6BAA6B,WAAA,EAAa;AACxC,UAAA,IAAI,yBAAA,EAA2B;AAC7B,YAAA,MAAM,IAAI,KAAA;AAAA,cACR;AAAA,aACF;AAAA,UACF;AACA,UAAA,yBAAA,GAA4B,WAAA;AAAA,QAC9B;AAAA;AACF,KACF;AAEA,IAAA,GAAA,CAAI,YAAA,CAAa;AAAA,MACf,IAAA,EAAM;AAAA,QACJ,QAAQC,6BAAA,CAAa,UAAA;AAAA,QACrB,QAAQA,6BAAA,CAAa,MAAA;AAAA,QACrB,WAAA,EAAaC;AAAA,OACf;AAAA,MACA,MAAM,IAAA,CAAK,EAAE,MAAA,EAAQ,MAAA,EAAQ,aAAY,EAAG;AAC1C,QAAA,MAAM,eAAA,GAAkBC,sDAAoB,MAAM,CAAA;AAElD,QAAA,KAAA,MAAW,kBAAkB,eAAA,EAAiB;AAC5C,UAAA,MAAM,QAAA,GAAW,IAAIC,+EAAA,CAAwC;AAAA,YAC3D,IAAI,cAAA,CAAe,EAAA;AAAA,YACnB,QAAA,EAAU,cAAA;AAAA,YACV,MAAA;AAAA,YACA,eAAA,EAAiB,kBAAA;AAAA,cACf,cAAA,CAAe,EAAA;AAAA,cACf;AAAA,aACF;AAAA,YACA,gBAAA,EAAkB,kBAAA;AAAA,cAChB,cAAA,CAAe,EAAA;AAAA,cACf;AAAA,aACF;AAAA,YACA,uBAAA,EAAyB,kBAAA;AAAA,cACvB,cAAA,CAAe,EAAA;AAAA,cACf;AAAA,aACF;AAAA,YACA,yBAAA,EAA2B,kBAAA;AAAA,cACzB,cAAA,CAAe,EAAA;AAAA,cACf;AAAA;AACF,WACD,CAAA;AAED,UAAA,MAAM,UAAA,GAAa,gBAAA,CAAiB,cAAA,EAAgB,MAAM,CAAA;AAE1D,UAAA,WAAA,CAAY,WAAA,CAA2C;AAAA,YACrD,QAAA;AAAA,YACA,OAAA,EAAS;AAAA,cACP,aAAA,EAAe,EAAE,OAAA,EAAS,CAAA,EAAE;AAAA,cAC5B,WAAA,EAAa,EAAE,OAAA,EAAS,CAAA,EAAE;AAAA,cAC1B,UAAA;AAAA,cACA,OAAA,EAAS;AAAA,gBACP,EAAE,SAAS,EAAA,EAAG;AAAA,gBACd,EAAE,SAAS,CAAA,EAAE;AAAA,gBACb,EAAE,SAAS,EAAA,EAAG;AAAA,gBACd,EAAE,OAAO,CAAA;AAAE;AACb;AACF,WACD,CAAA;AAAA,QACH;AAAA,MACF;AAAA,KACD,CAAA;AAAA,EACH;AACF,CAAC;AAEH,SAAS,kBAAA,CACP,IACA,WAAA,EACe;AACf,EAAA,IAAI,CAAC,WAAA,EAAa,UAAU,EAAE,QAAA,CAAS,OAAO,WAAW,CAAA,EAAG;AAC1D,IAAA,OAAO,WAAA;AAAA,EACT;AACA,EAAA,OAAQ,YAAkC,EAAE,CAAA;AAC9C;AAEA,SAAS,gBAAA,CACP,gBACA,MAAA,EACe;AACf,EAAA,MAAM,IAAA,GAAO,eAAe,QAAA,EAAU,SAAA;AAGtC,EAAA,IACE,IAAA,IACA,OAAO,IAAA,KAAS,QAAA,IAChB,EAAE,MAAA,IAAU,IAAA,CAAA,IACZ,EAAE,SAAA,IAAa,IAAA,CAAA,EACf;AACA,IAAA,OAAO,IAAA;AAAA,EACT;AACA,EAAA,IAAI,IAAA,EAAM;AACR,IAAA,MAAA,CAAO,IAAA;AAAA,MACL,CAAA,wCAAA,EAA2C,eAAe,EAAE,CAAA,0HAAA;AAAA,KAG9D;AAAA,EACF;AACA,EAAA,OAAO,EAAE,OAAO,CAAA,EAAE;AACpB;;;;;"}
|
package/package.json
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@backstage/plugin-catalog-backend-module-msgraph-incremental",
|
|
3
|
+
"version": "0.0.0-nightly-20260507032228",
|
|
4
|
+
"description": "A Backstage catalog backend module that incrementally ingests users and groups from Microsoft Graph",
|
|
5
|
+
"backstage": {
|
|
6
|
+
"role": "backend-plugin-module",
|
|
7
|
+
"pluginId": "catalog",
|
|
8
|
+
"pluginPackage": "@backstage/plugin-catalog-backend",
|
|
9
|
+
"features": {
|
|
10
|
+
".": "@backstage/BackendFeature"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"backstage"
|
|
18
|
+
],
|
|
19
|
+
"homepage": "https://backstage.io",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/backstage/backstage",
|
|
23
|
+
"directory": "plugins/catalog-backend-module-msgraph-incremental"
|
|
24
|
+
},
|
|
25
|
+
"license": "Apache-2.0",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"backstage": "@backstage/BackendFeature",
|
|
29
|
+
"require": "./dist/index.cjs.js",
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"default": "./dist/index.cjs.js"
|
|
32
|
+
},
|
|
33
|
+
"./alpha": {
|
|
34
|
+
"require": "./dist/alpha.cjs.js",
|
|
35
|
+
"types": "./dist/alpha.d.ts",
|
|
36
|
+
"default": "./dist/alpha.cjs.js"
|
|
37
|
+
},
|
|
38
|
+
"./package.json": "./package.json"
|
|
39
|
+
},
|
|
40
|
+
"main": "./dist/index.cjs.js",
|
|
41
|
+
"types": "./dist/index.d.ts",
|
|
42
|
+
"typesVersions": {
|
|
43
|
+
"*": {
|
|
44
|
+
"alpha": [
|
|
45
|
+
"dist/alpha.d.ts"
|
|
46
|
+
],
|
|
47
|
+
"package.json": [
|
|
48
|
+
"package.json"
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"files": [
|
|
53
|
+
"dist"
|
|
54
|
+
],
|
|
55
|
+
"scripts": {
|
|
56
|
+
"build": "backstage-cli package build",
|
|
57
|
+
"clean": "backstage-cli package clean",
|
|
58
|
+
"lint": "backstage-cli package lint",
|
|
59
|
+
"prepack": "backstage-cli package prepack",
|
|
60
|
+
"postpack": "backstage-cli package postpack",
|
|
61
|
+
"start": "backstage-cli package start",
|
|
62
|
+
"test": "backstage-cli package test"
|
|
63
|
+
},
|
|
64
|
+
"dependencies": {
|
|
65
|
+
"@backstage/backend-plugin-api": "0.0.0-nightly-20260507032228",
|
|
66
|
+
"@backstage/catalog-model": "0.0.0-nightly-20260507032228",
|
|
67
|
+
"@backstage/config": "0.0.0-nightly-20260507032228",
|
|
68
|
+
"@backstage/plugin-catalog-backend-module-incremental-ingestion": "0.0.0-nightly-20260507032228",
|
|
69
|
+
"@backstage/plugin-catalog-backend-module-msgraph": "0.0.0-nightly-20260507032228",
|
|
70
|
+
"@backstage/plugin-catalog-node": "0.0.0-nightly-20260507032228",
|
|
71
|
+
"@backstage/types": "1.2.2",
|
|
72
|
+
"@microsoft/microsoft-graph-types": "^2.6.0",
|
|
73
|
+
"p-limit": "^3.0.2"
|
|
74
|
+
},
|
|
75
|
+
"devDependencies": {
|
|
76
|
+
"@backstage/backend-test-utils": "0.0.0-nightly-20260507032228",
|
|
77
|
+
"@backstage/cli": "0.0.0-nightly-20260507032228"
|
|
78
|
+
}
|
|
79
|
+
}
|