Skip to content

Commit 8a56e0e

Browse files
Merge pull request #255 from microsoftgraph/FehintolaObafemi/Singleton-Support
2 parents a5c12f2 + 16130c4 commit 8a56e0e

File tree

7 files changed

+281
-10
lines changed

7 files changed

+281
-10
lines changed

msgraph-metadata

src/swagger-generation/src/config.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ export interface StreamPropertyConfig {
2424
}
2525

2626
export interface ResourceKeyConfig {
27-
Name: string
27+
Name: string,
28+
OmitInPayload?: boolean
2829
}
2930

3031
export interface OrchestrationPropertyConfig {
@@ -59,6 +60,8 @@ export interface EntityTypeConfig {
5960
EntitySetPath?: string
6061
ResourceKey?: ResourceKeyConfig
6162
OrchestrationProperties?: OrchestrationPropertiesConfig
63+
IsSingleton?: boolean,
64+
PathSegmentName?: string
6265
}
6366

6467
export class Config {

src/swagger-generation/src/definitions/Metadata.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ export interface StreamPropertyMetadata {
3939
}
4040

4141
export interface ResourceKey {
42-
name: string
42+
name: string,
43+
omitInPayload?: boolean
4344
}
4445

4546
export interface OrchestrationProperty {
@@ -68,5 +69,7 @@ export interface EntityMetadata {
6869
compositeKeyProperties?: string[],
6970
relationshipMetadata?: RelationshipMetadata,
7071
resourceKey?: ResourceKey,
71-
orchestrationProperties?: OrchestrationProperties
72+
orchestrationProperties?: OrchestrationProperties,
73+
isSingleton?: boolean,
74+
pathSegmentName?: string
7275
}

src/swagger-generation/src/metadataWriter.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,11 @@ export const writeMetadata = (definitionMap: DefinitionMap, config: Config): Met
6969
compositeKeyProperties: entityTypeConfig.CompositeKey,
7070
relationshipMetadata: getRelationshipMetadata(entityTypeConfig.Relationships, entity),
7171
resourceKey: entityTypeConfig.ResourceKey ? {
72-
name: entityTypeConfig.ResourceKey.Name
72+
name: entityTypeConfig.ResourceKey.Name,
73+
omitInPayload: entityTypeConfig.ResourceKey.OmitInPayload
7374
} : undefined,
75+
isSingleton: entityTypeConfig.IsSingleton,
76+
pathSegmentName: entityTypeConfig.PathSegmentName,
7477
orchestrationProperties: {
7578
save: orchestrationProperties.Save?.map(p => ({
7679
name: p.Name,

src/swagger-generation/src/swaggerWriter.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,22 +93,26 @@ export const writeSwagger = (definitionMap: DefinitionMap, config: Config): Swag
9393
if (!entityTypeConfig.RootUri) { // Entity is not exposed
9494
return;
9595
}
96+
9697
const entityName: string = definitionMap.EntityMap.get(id)!.Name
9798
const entitySegments: string[] = entityTypeConfig.RootUri.split("/").slice(-2)
9899
const operationType: string = entityTypeConfig.IsReadonlyResource ? "get" : "put";
99100
const operationDescription: string = entityTypeConfig.IsReadonlyResource ? "Get" : "Create or update";
100101
const parentEntity: string = entitySegments[0];
101102
const entitySet: string = entitySegments[1];
102103
let relativeUri: string = entitySet;
103-
let parameters: Parameter[] = [
104-
{
104+
let parameters: Parameter[] = [];
105+
106+
// For singleton resources, don't add ID parameter
107+
if (!entityTypeConfig.IsSingleton) {
108+
parameters.push({
105109
in: "path",
106110
description: `The id of the ${entityName}`,
107111
name: `${entityName}Id`,
108112
required: true,
109113
type: "string"
110-
},
111-
];
114+
});
115+
}
112116

113117
if (!entityTypeConfig.IsReadonlyResource) {
114118
parameters.push(
@@ -135,7 +139,9 @@ export const writeSwagger = (definitionMap: DefinitionMap, config: Config): Swag
135139
})
136140
};
137141

138-
const host: string = `/{rootScope}/providers/Microsoft.Graph/${relativeUri}/{${entityName}Id}`
142+
// For singleton resources, don't include the ID in the path
143+
const pathIdSegment = entityTypeConfig.IsSingleton ? "" : `/{${entityName}Id}`;
144+
const host: string = `/{rootScope}/providers/Microsoft.Graph/${relativeUri}${pathIdSegment}`
139145
const path: Path = {
140146
[operationType]: {
141147
tags: [

src/swagger-generation/tests/metadataWriter.test.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,4 +285,109 @@ describe("writeMetadata", () => {
285285
}
286286
});
287287
});
288+
289+
describe("singleton support", () => {
290+
it("should include singleton properties in metadata when IsSingleton is true", () => {
291+
const definitionMap: DefinitionMap = new DefinitionMap();
292+
const entityTypes: Map<string, EntityTypeConfig> = new Map<string, EntityTypeConfig>();
293+
294+
definitionMap.EntityMap.set(
295+
"microsoft.graph.admin",
296+
new EntityType("admin", undefined, false, undefined, false, false, [], [])
297+
);
298+
299+
entityTypes.set("microsoft.graph.admin", {
300+
Name: "microsoft.graph.admin",
301+
Upsertable: true,
302+
RootUri: "/admin",
303+
IsSingleton: true,
304+
PathSegmentName: "admin",
305+
EntitySetPath: "admin",
306+
NavigationProperty: [],
307+
} as EntityTypeConfig);
308+
309+
const config = {
310+
EntityTypes: entityTypes,
311+
MetadataFilePath: "https://example.com",
312+
APIVersion: "beta",
313+
} as Config;
314+
315+
const metadata = writeMetadata(definitionMap, config);
316+
317+
expect(metadata["admin"]).toBeDefined();
318+
expect(metadata["admin"]["beta"].isSingleton).toBe(true);
319+
expect(metadata["admin"]["beta"].pathSegmentName).toBe("admin");
320+
expect(metadata["admin"]["beta"].entitySetPath).toBe("admin");
321+
});
322+
323+
it("should not include singleton properties when IsSingleton is false", () => {
324+
const definitionMap: DefinitionMap = new DefinitionMap();
325+
const entityTypes: Map<string, EntityTypeConfig> = new Map<string, EntityTypeConfig>();
326+
327+
definitionMap.EntityMap.set(
328+
"microsoft.graph.user",
329+
new EntityType("user", undefined, false, undefined, false, false, [], [])
330+
);
331+
332+
entityTypes.set("microsoft.graph.user", {
333+
Name: "microsoft.graph.user",
334+
Upsertable: true,
335+
RootUri: "/users",
336+
IsSingleton: false,
337+
NavigationProperty: [],
338+
} as EntityTypeConfig);
339+
340+
const config = {
341+
EntityTypes: entityTypes,
342+
MetadataFilePath: "https://example.com",
343+
APIVersion: "beta",
344+
} as Config;
345+
346+
const metadata = writeMetadata(definitionMap, config);
347+
348+
expect(metadata["users"]).toBeDefined();
349+
expect(metadata["users"]["beta"].isSingleton).toBe(false);
350+
expect(metadata["users"]["beta"].pathSegmentName).toBeUndefined();
351+
});
352+
353+
it("should include resourceKey with omitInPayload when configured", () => {
354+
const definitionMap: DefinitionMap = new DefinitionMap();
355+
const entityTypes: Map<string, EntityTypeConfig> = new Map<string, EntityTypeConfig>();
356+
357+
definitionMap.EntityMap.set(
358+
"microsoft.graph.testSingletonEntity",
359+
new EntityType("testSingletonEntity", undefined, false, undefined, false, false, [], [])
360+
);
361+
362+
entityTypes.set("microsoft.graph.testSingletonEntity", {
363+
Name: "microsoft.graph.testSingletonEntity",
364+
Upsertable: true,
365+
RootUri: "/testContainer/testSingleton",
366+
IsSingleton: true,
367+
PathSegmentName: "testSingleton",
368+
EntitySetPath: "testContainer/testSingleton",
369+
ResourceKey: {
370+
Name: "testKey",
371+
OmitInPayload: true
372+
},
373+
NavigationProperty: [],
374+
} as EntityTypeConfig);
375+
376+
const config = {
377+
EntityTypes: entityTypes,
378+
MetadataFilePath: "https://example.com",
379+
APIVersion: "beta",
380+
} as Config;
381+
382+
const metadata = writeMetadata(definitionMap, config);
383+
384+
expect(metadata["testContainer/testSingleton"]).toBeDefined();
385+
expect(metadata["testContainer/testSingleton"]["beta"].isSingleton).toBe(true);
386+
expect(metadata["testContainer/testSingleton"]["beta"].pathSegmentName).toBe("testSingleton");
387+
expect(metadata["testContainer/testSingleton"]["beta"].resourceKey).toEqual({
388+
name: "testKey",
389+
omitInPayload: true
390+
});
391+
});
392+
});
288393
});

src/swagger-generation/tests/swaggerWriter.test.ts

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1385,5 +1385,156 @@ describe('enums', () => {
13851385

13861386
expect(writeSwagger(definitionMap, config)).toEqual(expectedSwagger);
13871387
});
1388+
});
1389+
1390+
describe('singleton resource support', () => {
1391+
it('should generate swagger path without ID parameter for singleton resources', () => {
1392+
const definitionMap: DefinitionMap = new DefinitionMap();
1393+
const entityMap: EntityMap = new Map<string, EntityType>();
1394+
1395+
const adminEntity = new EntityType('admin', undefined, false, undefined, false, false, [], []);
1396+
entityMap.set('microsoft.graph.admin', adminEntity);
1397+
1398+
const entityTypes: Map<string, EntityTypeConfig> = new Map<string, EntityTypeConfig>();
1399+
entityTypes.set('microsoft.graph.admin', {
1400+
Name: 'microsoft.graph.admin',
1401+
RootUri: '/admin',
1402+
Upsertable: true,
1403+
IsSingleton: true,
1404+
PathSegmentName: 'admin',
1405+
EntitySetPath: 'admin',
1406+
NavigationProperty: []
1407+
} as EntityTypeConfig);
1408+
1409+
const config = {
1410+
ExtensionVersion: "1.0.0",
1411+
EntityTypes: entityTypes,
1412+
MetadataFilePath: 'https://example.com',
1413+
APIVersion: 'beta'
1414+
} as Config;
1415+
1416+
definitionMap.EntityMap = entityMap;
1417+
definitionMap.EnumMap = new Map();
1418+
1419+
const result = writeSwagger(definitionMap, config);
1420+
1421+
// Check that the singleton path exists without ID parameter
1422+
expect(result.paths['/{rootScope}/providers/Microsoft.Graph/admin']).toBeDefined();
1423+
1424+
// Check that the operation exists
1425+
const adminPath = result.paths['/{rootScope}/providers/Microsoft.Graph/admin'];
1426+
expect(adminPath.put).toBeDefined();
1427+
1428+
// Check that no ID parameter is included for singleton
1429+
if (adminPath.put) {
1430+
const parameters = adminPath.put.parameters;
1431+
const idParameter = parameters.find(p => p.name === 'adminId');
1432+
expect(idParameter).toBeUndefined();
1433+
1434+
// Should still have body parameter
1435+
const bodyParameter = parameters.find(p => p.in === 'body');
1436+
expect(bodyParameter).toBeDefined();
1437+
}
1438+
});
13881439

1440+
it('should generate swagger path with ID parameter for non-singleton resources', () => {
1441+
const definitionMap: DefinitionMap = new DefinitionMap();
1442+
const entityMap: EntityMap = new Map<string, EntityType>();
1443+
1444+
const userEntity = new EntityType('user', undefined, false, undefined, false, false, [], []);
1445+
entityMap.set('microsoft.graph.user', userEntity);
1446+
1447+
const entityTypes: Map<string, EntityTypeConfig> = new Map<string, EntityTypeConfig>();
1448+
entityTypes.set('microsoft.graph.user', {
1449+
Name: 'microsoft.graph.user',
1450+
RootUri: '/users',
1451+
Upsertable: true,
1452+
IsSingleton: false,
1453+
NavigationProperty: []
1454+
} as EntityTypeConfig);
1455+
1456+
const config = {
1457+
ExtensionVersion: "1.0.0",
1458+
EntityTypes: entityTypes,
1459+
MetadataFilePath: 'https://example.com',
1460+
APIVersion: 'beta'
1461+
} as Config;
1462+
1463+
definitionMap.EntityMap = entityMap;
1464+
definitionMap.EnumMap = new Map();
1465+
1466+
const result = writeSwagger(definitionMap, config);
1467+
1468+
// Check that the regular path exists with ID parameter
1469+
expect(result.paths['/{rootScope}/providers/Microsoft.Graph/users/{userId}']).toBeDefined();
1470+
1471+
// Check that the operation exists
1472+
const userPath = result.paths['/{rootScope}/providers/Microsoft.Graph/users/{userId}'];
1473+
expect(userPath.put).toBeDefined();
1474+
1475+
// Check that ID parameter is included for non-singleton
1476+
if (userPath.put) {
1477+
const parameters = userPath.put.parameters;
1478+
const idParameter = parameters.find(p => p.name === 'userId');
1479+
expect(idParameter).toBeDefined();
1480+
if (idParameter) {
1481+
expect(idParameter.in).toBe('path');
1482+
expect(idParameter.required).toBe(true);
1483+
}
1484+
}
1485+
});
1486+
1487+
it('should generate swagger for container singleton resources', () => {
1488+
const definitionMap: DefinitionMap = new DefinitionMap();
1489+
const entityMap: EntityMap = new Map<string, EntityType>();
1490+
1491+
const domainRegEntity = new EntityType('domainRegistration', undefined, false, undefined, false, false, [], []);
1492+
entityMap.set('microsoft.graph.domainRegistration', domainRegEntity);
1493+
1494+
const entityTypes: Map<string, EntityTypeConfig> = new Map<string, EntityTypeConfig>();
1495+
entityTypes.set('microsoft.graph.domainRegistration', {
1496+
Name: 'microsoft.graph.domainRegistration',
1497+
RootUri: '/applications/domainRegistration',
1498+
Upsertable: true,
1499+
IsSingleton: true,
1500+
PathSegmentName: 'domainRegistration',
1501+
EntitySetPath: 'applications/domainRegistration',
1502+
ContainerEntitySet: 'applications',
1503+
NavigationProperty: []
1504+
} as EntityTypeConfig);
1505+
1506+
const config = {
1507+
ExtensionVersion: "1.0.0",
1508+
EntityTypes: entityTypes,
1509+
MetadataFilePath: 'https://example.com',
1510+
APIVersion: 'beta'
1511+
} as Config;
1512+
1513+
definitionMap.EntityMap = entityMap;
1514+
definitionMap.EnumMap = new Map();
1515+
1516+
const result = writeSwagger(definitionMap, config);
1517+
1518+
// Check that the container singleton path exists without ID parameter
1519+
expect(result.paths['/{rootScope}/providers/Microsoft.Graph/applications/{applicationsId}/domainRegistration']).toBeDefined();
1520+
1521+
// Check that the operation exists
1522+
const domainRegPath = result.paths['/{rootScope}/providers/Microsoft.Graph/applications/{applicationsId}/domainRegistration'];
1523+
expect(domainRegPath.put).toBeDefined();
1524+
1525+
// Check that no ID parameter is included for the singleton itself
1526+
if (domainRegPath.put) {
1527+
const parameters = domainRegPath.put.parameters;
1528+
const singletonIdParameter = parameters.find(p => p.name === 'domainRegistrationId');
1529+
expect(singletonIdParameter).toBeUndefined();
1530+
1531+
// Should still have container ID parameter
1532+
const containerIdParameter = parameters.find(p => p.name === 'applicationsId');
1533+
expect(containerIdParameter).toBeDefined();
1534+
1535+
// Should still have body parameter
1536+
const bodyParameter = parameters.find(p => p.in === 'body');
1537+
expect(bodyParameter).toBeDefined();
1538+
}
1539+
});
13891540
});

0 commit comments

Comments
 (0)