Skip to content

Commit dce85d0

Browse files
Use SharePoint APIs to sync content types
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent bfe7f14 commit dce85d0

3 files changed

Lines changed: 39 additions & 61 deletions

File tree

docs/docs/cmd/spo/contenttype/contenttype-sync.mdx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ This command will only return a response if the content type is added to the sit
8282

8383
```json
8484
{
85-
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#contentType",
85+
"@odata.context": "https://contoso.sharepoint.com/_api/v2.0/$metadata#contentType",
8686
"@odata.type": "#microsoft.graph.contentType",
8787
"@odata.etag": "\"2\"",
8888
"id": "0x0101003D510887202C894AB7CD88CCE011519D",
@@ -110,7 +110,7 @@ This command will only return a response if the content type is added to the sit
110110
<TabItem value="Text">
111111

112112
```text
113-
@odata.context: https://graph.microsoft.com/v1.0/$metadata#contentType
113+
@odata.context: https://contoso.sharepoint.com/_api/v2.0/$metadata#contentType
114114
@odata.etag : "2"
115115
@odata.type : #microsoft.graph.contentType
116116
base : {"id":"0x0101","description":"Create a new document.","group":"Document Content Types","hidden":false,"name":"Document","readOnly":false,"sealed":false}
@@ -130,7 +130,7 @@ This command will only return a response if the content type is added to the sit
130130

131131
```csv
132132
@odata.context,@odata.type,@odata.etag,id,isBuiltIn,description,group,hidden,name,parentId,readOnly,sealed
133-
https://graph.microsoft.com/v1.0/$metadata#contentType,#microsoft.graph.contentType,"""2""",0x010100F4ACBE99F2DBDE42B0487A561A40F9EE,,,Custom Content Types,,Dummy,0x0101,1,
133+
https://contoso.sharepoint.com/_api/v2.0/$metadata#contentType,#microsoft.graph.contentType,"""2""",0x010100F4ACBE99F2DBDE42B0487A561A40F9EE,,,Custom Content Types,,Dummy,0x0101,1,
134134
```
135135

136136
</TabItem>
@@ -145,7 +145,7 @@ This command will only return a response if the content type is added to the sit
145145

146146
Property | Value
147147
---------|-------
148-
@odata.context | https://graph.microsoft.com/v1.0/$metadata#contentType
148+
@odata.context | https://contoso.sharepoint.com/_api/v2.0/$metadata#contentType
149149
@odata.type | #microsoft.graph.contentType
150150
@odata.etag | "2"
151151
id | 0x010100F4ACBE99F2DBDE42B0487A561A40F9EE

src/m365/spo/commands/contenttype/contenttype-sync.spec.ts

Lines changed: 23 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,13 @@ describe(commands.CONTENTTYPE_SYNC, () => {
2323
const listId = 'd4552f22-5fb0-4df3-b216-309264237d2b';
2424
const listTitle = 'Documents';
2525
const listUrl = '/sites/project-x/Shared Documents';
26-
const graphBaseUrl = 'https://graph.microsoft.com/v1.0/sites/';
27-
const siteId = 'contoso.sharepoint.com,777794dc-43c9-4f36-88bd-a42721c75304,95c1171f-7b40-46bb-8679-3b5263dc019a';
26+
const siteId = '777794dc-43c9-4f36-88bd-a42721c75304';
27+
const webId = '95c1171f-7b40-46bb-8679-3b5263dc019a';
28+
const siteResource = `contoso.sharepoint.com,${siteId},${webId}`;
29+
const spApiBaseUrl = 'https://contoso.sharepoint.com/_api/v2.0/sites/';
2830

2931
const syncResponse = {
30-
'@odata.context': 'https://graph.microsoft.com/v1.0/$metadata#contentType',
32+
'@odata.context': 'https://contoso.sharepoint.com/_api/v2.0/$metadata#contentType',
3133
'@odata.type': '#microsoft.graph.contentType',
3234
'@odata.etag': '\'2\'',
3335
id: '0x010100B01336624A574D47BE29121892EA4D98',
@@ -60,7 +62,9 @@ describe(commands.CONTENTTYPE_SYNC, () => {
6062
sinon.stub(telemetry, 'trackEvent').resolves();
6163
sinon.stub(pid, 'getProcessName').returns('');
6264
sinon.stub(session, 'getId').returns('');
63-
sinon.stub(spo, 'getSiteIdByMSGraph').resolves(siteId);
65+
sinon.stub(spo, 'getSiteIdBySPApi').resolves(siteId);
66+
sinon.stub(spo, 'getWebId').resolves(webId);
67+
sinon.stub(spo, 'getListId').resolves(listId);
6468
auth.connection.active = true;
6569
commandInfo = cli.getCommandInfo(command);
6670
});
@@ -103,7 +107,7 @@ describe(commands.CONTENTTYPE_SYNC, () => {
103107

104108
it('succesfully sync a content type to the site by id', async () => {
105109
sinon.stub(request, 'post').callsFake(async (opts) => {
106-
if (opts.url === `${graphBaseUrl}${siteId}/contenttypes/addCopyFromContentTypeHub` && opts.data.contentTypeId === contentTypeId) {
110+
if (opts.url === `${spApiBaseUrl}${siteResource}/contenttypes/addCopyFromContentTypeHub` && opts.data.contentTypeId === contentTypeId) {
107111
return syncResponse;
108112
}
109113
throw 'Invalid request';
@@ -115,16 +119,16 @@ describe(commands.CONTENTTYPE_SYNC, () => {
115119

116120
it('succesfully sync a content type to the site by name', async () => {
117121
sinon.stub(request, 'post').callsFake(async (opts) => {
118-
if (opts.url === `${graphBaseUrl}${siteId}/contenttypes/addCopyFromContentTypeHub` && opts.data.contentTypeId === contentTypeId) {
122+
if (opts.url === `${spApiBaseUrl}${siteResource}/contenttypes/addCopyFromContentTypeHub` && opts.data.contentTypeId === contentTypeId) {
119123
return syncResponse;
120124
}
121125

122126
throw 'Invalid request';
123127
});
124128

125129
sinon.stub(odata, 'getAllItems').callsFake(async (url: string) => {
126-
if (url === `${graphBaseUrl}${siteId}/contenttypes?$filter=name eq '${contentTypeName}'&$select=id,name`) {
127-
return [{ id: contentTypeId }];
130+
if (url === `${webUrl}/_api/web/AvailableContentTypes?$filter=Name eq '${formatting.encodeQueryParameter(contentTypeName)}'&$select=StringId,Name`) {
131+
return [{ StringId: contentTypeId }];
128132
}
129133

130134
throw 'Invalid request';
@@ -136,16 +140,16 @@ describe(commands.CONTENTTYPE_SYNC, () => {
136140

137141
it('succesfully sync a content type to the site by name after the initial sync', async () => {
138142
const postStub = sinon.stub(request, 'post').callsFake(async (opts) => {
139-
if (opts.url === `${graphBaseUrl}${siteId}/contenttypes/addCopyFromContentTypeHub` && opts.data.contentTypeId === contentTypeId) {
143+
if (opts.url === `${spApiBaseUrl}${siteResource}/contenttypes/addCopyFromContentTypeHub` && opts.data.contentTypeId === contentTypeId) {
140144
return '';
141145
}
142146

143147
throw 'Invalid request';
144148
});
145149

146150
sinon.stub(odata, 'getAllItems').callsFake(async (url: string) => {
147-
if (url === `${graphBaseUrl}${siteId}/contenttypes?$filter=name eq '${contentTypeName}'&$select=id,name`) {
148-
return [{ id: contentTypeId }];
151+
if (url === `${webUrl}/_api/web/AvailableContentTypes?$filter=Name eq '${formatting.encodeQueryParameter(contentTypeName)}'&$select=StringId,Name`) {
152+
return [{ StringId: contentTypeId }];
149153
}
150154

151155
throw 'Invalid request';
@@ -158,16 +162,16 @@ describe(commands.CONTENTTYPE_SYNC, () => {
158162
it('succesfully sync a content type to a list by listId', async () => {
159163
const url = webUrl.split('/sites/')[0];
160164
sinon.stub(request, 'post').callsFake(async (opts) => {
161-
if (opts.url === `${graphBaseUrl}${new URL(url).host}/lists/${listId}/contenttypes/addCopyFromContentTypeHub` && opts.data.contentTypeId === contentTypeId) {
165+
if (opts.url === `${spApiBaseUrl}${siteResource}/lists/${listId}/contenttypes/addCopyFromContentTypeHub` && opts.data.contentTypeId === contentTypeId) {
162166
return syncResponse;
163167
}
164168

165169
throw 'Invalid request';
166170
});
167171

168172
sinon.stub(odata, 'getAllItems').callsFake(async (url: string) => {
169-
if (url === `${graphBaseUrl}${siteId}/contenttypes?$filter=name eq '${contentTypeName}'&$select=id,name`) {
170-
return [{ id: contentTypeId }];
173+
if (url === `${new URL(webUrl).origin}/_api/web/AvailableContentTypes?$filter=Name eq '${formatting.encodeQueryParameter(contentTypeName)}'&$select=StringId,Name`) {
174+
return [{ StringId: contentTypeId }];
171175
}
172176

173177
throw 'Invalid request';
@@ -179,7 +183,7 @@ describe(commands.CONTENTTYPE_SYNC, () => {
179183

180184
it('succesfully sync a content type to a list by listTitle', async () => {
181185
sinon.stub(request, 'post').callsFake(async (opts) => {
182-
if (opts.url === `${graphBaseUrl}${siteId}/lists/${listTitle}/contenttypes/addCopyFromContentTypeHub` && opts.data.contentTypeId === contentTypeId) {
186+
if (opts.url === `${spApiBaseUrl}${siteResource}/lists/${listId}/contenttypes/addCopyFromContentTypeHub` && opts.data.contentTypeId === contentTypeId) {
183187
return syncResponse;
184188
}
185189

@@ -192,28 +196,20 @@ describe(commands.CONTENTTYPE_SYNC, () => {
192196

193197
it('succesfully sync a content type to a list by listUrl', async () => {
194198
sinon.stub(request, 'post').callsFake(async (opts) => {
195-
if (opts.url === `${graphBaseUrl}${siteId}/lists/${listId}/contenttypes/addCopyFromContentTypeHub` && opts.data.contentTypeId === contentTypeId) {
199+
if (opts.url === `${spApiBaseUrl}${siteResource}/lists/${listId}/contenttypes/addCopyFromContentTypeHub` && opts.data.contentTypeId === contentTypeId) {
196200
return syncResponse;
197201
}
198202

199203
throw 'Invalid request';
200204
});
201205

202-
sinon.stub(request, 'get').callsFake(async (opts) => {
203-
if (opts.url === `${webUrl}/_api/web/GetList('${formatting.encodeQueryParameter(listUrl)}')?$select=id`) {
204-
return { Id: listId };
205-
}
206-
207-
throw 'Invalid request';
208-
});
209-
210206
await command.action(logger, { options: { webUrl: webUrl, id: contentTypeId, listUrl: listUrl, verbose: true } } as any);
211207
assert(loggerLogSpy.calledOnceWithExactly(syncResponse));
212208
});
213209

214210
it('correctly handles contentType not found in the hub', async () => {
215211
sinon.stub(odata, 'getAllItems').callsFake(async (url: string) => {
216-
if (url === `${graphBaseUrl}${siteId}/contenttypes?$filter=name eq '${contentTypeName}'&$select=id,name`) {
212+
if (url === `${webUrl}/_api/web/AvailableContentTypes?$filter=Name eq '${formatting.encodeQueryParameter(contentTypeName)}'&$select=StringId,Name`) {
217213
return [];
218214
}
219215

@@ -238,7 +234,7 @@ describe(commands.CONTENTTYPE_SYNC, () => {
238234
};
239235

240236
sinon.stub(request, 'post').callsFake(async (opts) => {
241-
if (opts.url === `${graphBaseUrl}${siteId}/contenttypes/addCopyFromContentTypeHub` && opts.data.contentTypeId === contentTypeId) {
237+
if (opts.url === `${spApiBaseUrl}${siteResource}/contenttypes/addCopyFromContentTypeHub` && opts.data.contentTypeId === contentTypeId) {
242238
throw error;
243239
}
244240

@@ -263,7 +259,7 @@ describe(commands.CONTENTTYPE_SYNC, () => {
263259
};
264260

265261
sinon.stub(request, 'post').callsFake(async (opts) => {
266-
if (opts.url === `${graphBaseUrl}${siteId}/contenttypes/addCopyFromContentTypeHub` && opts.data.contentTypeId === contentTypeId) {
262+
if (opts.url === `${spApiBaseUrl}${siteResource}/contenttypes/addCopyFromContentTypeHub` && opts.data.contentTypeId === contentTypeId) {
267263
throw error;
268264
}
269265

src/m365/spo/commands/contenttype/contenttype-sync.ts

Lines changed: 12 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import request, { CliRequestOptions } from '../../../../request.js';
55
import { validation } from '../../../../utils/validation.js';
66
import SpoCommand from '../../../base/SpoCommand.js';
77
import commands from '../../commands.js';
8-
import { urlUtil } from '../../../../utils/urlUtil.js';
98
import { odata } from '../../../../utils/odata.js';
109
import { spo } from '../../../../utils/spo.js';
1110

@@ -112,12 +111,17 @@ class SpoContentTypeSyncCommand extends SpoCommand {
112111
public async commandAction(logger: Logger, args: CommandArgs): Promise<void> {
113112
const { listId, listTitle, listUrl, webUrl } = args.options;
114113
const url: URL = new URL(webUrl);
115-
const baseUrl = 'https://graph.microsoft.com/v1.0/sites/';
114+
const baseUrl = `${url.origin}/_api/v2.0/sites/`;
116115

117116
try {
118-
const siteUrl = url.pathname === '/' ? url.host : await spo.getSiteIdByMSGraph(webUrl, logger, this.verbose);
119-
const listPath = listId || listTitle || listUrl ? `/lists/${listId || listTitle || await this.getListIdByUrl(webUrl, listUrl!, logger)}` : '';
120-
const contentTypeId = await this.getContentTypeId(baseUrl, url, args.options, logger);
117+
const [siteId, webId] = await Promise.all([
118+
spo.getSiteIdBySPApi(webUrl, logger, this.verbose),
119+
spo.getWebId(webUrl, logger, this.verbose)
120+
]);
121+
const siteUrl = `${url.host},${siteId},${webId}`;
122+
const targetListId = listId || (listTitle || listUrl ? await spo.getListId(webUrl, listTitle, listUrl, logger, this.verbose) : undefined);
123+
const listPath = targetListId ? `/lists/${targetListId}` : '';
124+
const contentTypeId = await this.getContentTypeId(webUrl, args.options, logger);
121125

122126
if (this.verbose) {
123127
await logger.logToStderr(`Adding or syncing the content type...`);
@@ -147,44 +151,22 @@ class SpoContentTypeSyncCommand extends SpoCommand {
147151
}
148152
}
149153

150-
private async getContentTypeId(baseUrl: string, url: URL, options: Options, logger: Logger): Promise<string> {
154+
private async getContentTypeId(webUrl: string, options: Options, logger: Logger): Promise<string> {
151155
if (options.id) {
152156
return options.id;
153157
}
154158

155-
const siteId = await spo.getSiteIdByMSGraph(`${url.origin}/sites/contenttypehub`, logger, this.verbose);
156-
157159
if (this.verbose) {
158160
await logger.logToStderr(`Retrieving content type Id by name...`);
159161
}
160162

161-
const contentTypes: { id: string }[] = await odata.getAllItems(`${baseUrl}${siteId}/contenttypes?$filter=name eq '${options.name}'&$select=id,name`);
163+
const contentTypes: { StringId: string }[] = await odata.getAllItems(`${webUrl}/_api/web/AvailableContentTypes?$filter=Name eq '${formatting.encodeQueryParameter(options.name!)}'&$select=StringId,Name`);
162164

163165
if (contentTypes.length === 0) {
164166
throw `Content type with name ${options.name} not found.`;
165167
}
166168

167-
return contentTypes[0].id;
168-
}
169-
170-
private async getListIdByUrl(webUrl: string, listUrl: string, logger: Logger): Promise<string> {
171-
if (this.verbose) {
172-
await logger.logToStderr(`Retrieving list id to sync the content type to...`);
173-
}
174-
175-
const listServerRelativeUrl: string = urlUtil.getServerRelativePath(webUrl, listUrl);
176-
177-
const requestOptions: CliRequestOptions = {
178-
url: `${webUrl}/_api/web/GetList('${formatting.encodeQueryParameter(listServerRelativeUrl)}')?$select=id`,
179-
headers: {
180-
'accept': 'application/json;odata=nometadata'
181-
},
182-
responseType: 'json'
183-
};
184-
185-
const response = await request.get<{ Id: string }>(requestOptions);
186-
187-
return response.Id;
169+
return contentTypes[0].StringId;
188170
}
189171
}
190172

0 commit comments

Comments
 (0)