@the-vcsi/msgraph 0.0.1
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/package.json +19 -0
- package/src/index.js +64 -0
- package/src/templates/.env.example +6 -0
- package/src/templates/appSettings.js +14 -0
- package/src/templates/fetch-msgraph.js +156 -0
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@the-vcsi/msgraph",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "sv CLI add-on for Microsoft Graph / SharePoint integration",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": ["src"],
|
|
10
|
+
"keywords": ["sv-add", "svelte", "sveltekit", "microsoft-graph", "sharepoint"],
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"sv": "^0.11.0"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/Vermont-Complex-Systems/vcsi-starter"
|
|
17
|
+
},
|
|
18
|
+
"license": "MIT"
|
|
19
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { defineAddon, defineAddonOptions } from 'sv/core';
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
3
|
+
import { dirname, join } from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
8
|
+
// Read templates from files (testable, lintable)
|
|
9
|
+
const FETCH_SCRIPT = readFileSync(join(__dirname, 'templates/fetch-msgraph.js'), 'utf8');
|
|
10
|
+
const APP_SETTINGS = readFileSync(join(__dirname, 'templates/appSettings.js'), 'utf8');
|
|
11
|
+
const ENV_EXAMPLE = readFileSync(join(__dirname, 'templates/.env.example'), 'utf8');
|
|
12
|
+
|
|
13
|
+
const options = defineAddonOptions()
|
|
14
|
+
.add('siteId', {
|
|
15
|
+
question: 'SharePoint site ID (get from Graph Explorer or Azure Portal):',
|
|
16
|
+
type: 'string'
|
|
17
|
+
})
|
|
18
|
+
.build();
|
|
19
|
+
|
|
20
|
+
export default defineAddon({
|
|
21
|
+
id: '@the-vcsi/msgraph',
|
|
22
|
+
options,
|
|
23
|
+
|
|
24
|
+
run: ({ sv, options: opts }) => {
|
|
25
|
+
// Create the fetch script
|
|
26
|
+
sv.file('scripts/fetch-msgraph.js', () => FETCH_SCRIPT);
|
|
27
|
+
|
|
28
|
+
// Create app settings config with user-provided siteId
|
|
29
|
+
const appSettings = APP_SETTINGS.replace(/__SITE_ID__/g, opts.siteId || 'YOUR_SITE_ID');
|
|
30
|
+
sv.file('src/appSettings.js', () => appSettings);
|
|
31
|
+
|
|
32
|
+
// Append to .env.example (or create if doesn't exist)
|
|
33
|
+
sv.file('.env.example', (content) => {
|
|
34
|
+
if (content && content.includes('tenantId')) {
|
|
35
|
+
return content; // Already has msgraph config
|
|
36
|
+
}
|
|
37
|
+
return (content || '') + '\n' + ENV_EXAMPLE;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Add npm script and dependencies
|
|
41
|
+
sv.file('package.json', (content) => {
|
|
42
|
+
const pkg = JSON.parse(content);
|
|
43
|
+
pkg.scripts = pkg.scripts || {};
|
|
44
|
+
pkg.scripts['fetch:sharepoint'] = 'node scripts/fetch-msgraph.js';
|
|
45
|
+
|
|
46
|
+
// Add required dependencies
|
|
47
|
+
pkg.dependencies = pkg.dependencies || {};
|
|
48
|
+
pkg.dependencies['@azure/identity'] = '^4.0.0';
|
|
49
|
+
pkg.dependencies['@microsoft/microsoft-graph-client'] = '^3.0.0';
|
|
50
|
+
pkg.dependencies['dotenv'] = '^16.0.0';
|
|
51
|
+
|
|
52
|
+
return JSON.stringify(pkg, null, 2);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
console.log('\n Microsoft Graph integration added!');
|
|
56
|
+
console.log(' 1. Copy .env.example to .env');
|
|
57
|
+
console.log(' 2. Get credentials from Azure Portal > App registrations:');
|
|
58
|
+
console.log(' - tenantId: Directory (tenant) ID');
|
|
59
|
+
console.log(' - clientId: Application (client) ID');
|
|
60
|
+
console.log(' - clientSecret: Certificates & secrets > New client secret');
|
|
61
|
+
console.log(' 3. Run: npm install');
|
|
62
|
+
console.log(' 4. Run: npm run fetch:sharepoint');
|
|
63
|
+
}
|
|
64
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// https://learn.microsoft.com/en-us/graph/tutorials/javascript?tabs=aad
|
|
2
|
+
// Configuration for Microsoft Graph API
|
|
3
|
+
|
|
4
|
+
export const settings = {
|
|
5
|
+
clientId: process.env.clientId,
|
|
6
|
+
tenantId: process.env.tenantId,
|
|
7
|
+
clientSecret: process.env.clientSecret,
|
|
8
|
+
// SharePoint site ID - get from Graph Explorer or Azure Portal
|
|
9
|
+
siteId: '__SITE_ID__',
|
|
10
|
+
// Default scopes for client credentials flow
|
|
11
|
+
scopes: ['https://graph.microsoft.com/.default'],
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export default settings;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import 'dotenv/config';
|
|
4
|
+
import { writeFileSync, mkdirSync } from 'fs';
|
|
5
|
+
import { join, dirname } from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import { ClientSecretCredential } from '@azure/identity';
|
|
8
|
+
import { Client } from '@microsoft/microsoft-graph-client';
|
|
9
|
+
import { TokenCredentialAuthenticationProvider } from '@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials/index.js';
|
|
10
|
+
import settings from '../src/appSettings.js';
|
|
11
|
+
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const STORIES_DIR = join(__dirname, '..', 'src', 'lib', 'stories');
|
|
14
|
+
const DATA_STORY_FILE = 'data-story.xlsx';
|
|
15
|
+
|
|
16
|
+
function createClient() {
|
|
17
|
+
const credential = new ClientSecretCredential(
|
|
18
|
+
settings.tenantId || process.env.tenantId,
|
|
19
|
+
settings.clientId || process.env.clientId,
|
|
20
|
+
settings.clientSecret || process.env.clientSecret
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const authProvider = new TokenCredentialAuthenticationProvider(credential, {
|
|
24
|
+
scopes: settings.scopes,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
return Client.initWithMiddleware({ authProvider });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function sanitizeFilename(name) {
|
|
31
|
+
return name
|
|
32
|
+
.replace(/\.(xlsx|xlsm|xls)$/i, '')
|
|
33
|
+
.replace(/[^a-zA-Z0-9-_]/g, '-')
|
|
34
|
+
.toLowerCase();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function excelDateToString(serial) {
|
|
38
|
+
const utcDays = Math.floor(serial - 25569);
|
|
39
|
+
const date = new Date(utcDays * 86400 * 1000);
|
|
40
|
+
return date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function convertToStoryJson(rows) {
|
|
44
|
+
const [header, ...dataRows] = rows;
|
|
45
|
+
|
|
46
|
+
const result = {
|
|
47
|
+
title: '',
|
|
48
|
+
subtitle: '',
|
|
49
|
+
authors: [],
|
|
50
|
+
date: '',
|
|
51
|
+
SectionTitle: ''
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
let currentAuthor = {};
|
|
55
|
+
|
|
56
|
+
for (const row of dataRows) {
|
|
57
|
+
const section = String(row[0] || '').trim().toLowerCase();
|
|
58
|
+
const key = String(row[1] || '').trim();
|
|
59
|
+
const value = row[2];
|
|
60
|
+
|
|
61
|
+
if (!section || !key) continue;
|
|
62
|
+
|
|
63
|
+
if (section === 'meta') {
|
|
64
|
+
const keyLower = key.toLowerCase();
|
|
65
|
+
if (keyLower === 'title') result.title = value;
|
|
66
|
+
else if (keyLower === 'subtitle') result.subtitle = value;
|
|
67
|
+
else if (keyLower === 'sectiontitle') result.SectionTitle = value;
|
|
68
|
+
else if (keyLower === 'date') {
|
|
69
|
+
result.date = typeof value === 'number' ? excelDateToString(value) : value;
|
|
70
|
+
}
|
|
71
|
+
} else if (section === 'author') {
|
|
72
|
+
if (key.toLowerCase() === 'name') {
|
|
73
|
+
currentAuthor = { name: value };
|
|
74
|
+
} else if (key.toLowerCase() === 'url') {
|
|
75
|
+
currentAuthor.url = value;
|
|
76
|
+
result.authors.push(currentAuthor);
|
|
77
|
+
currentAuthor = {};
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
if (!result[section]) {
|
|
81
|
+
result[section] = [];
|
|
82
|
+
}
|
|
83
|
+
result[section].push({ type: key, value: String(value) });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function main() {
|
|
91
|
+
const client = createClient();
|
|
92
|
+
|
|
93
|
+
let site;
|
|
94
|
+
try {
|
|
95
|
+
site = await client.api(`/sites/${settings.siteId}`).get();
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error(`Error: ${error.code} - ${error.message}`);
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log(`Site: ${site.webUrl}`);
|
|
102
|
+
|
|
103
|
+
const drives = await client.api(`/sites/${site.id}/drives`).get();
|
|
104
|
+
const driveId = drives.value[0].id;
|
|
105
|
+
|
|
106
|
+
const items = await client.api(`/drives/${driveId}/root/children`).get();
|
|
107
|
+
const dataStoryFile = items.value.find(
|
|
108
|
+
(item) => item.file && item.name.toLowerCase() === DATA_STORY_FILE.toLowerCase()
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
if (!dataStoryFile) {
|
|
112
|
+
console.log(`\n${DATA_STORY_FILE} not found`);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
console.log(`\nProcessing: ${dataStoryFile.name}`);
|
|
117
|
+
|
|
118
|
+
const worksheets = await client
|
|
119
|
+
.api(`/drives/${driveId}/items/${dataStoryFile.id}/workbook/worksheets`)
|
|
120
|
+
.get();
|
|
121
|
+
|
|
122
|
+
for (const sheet of worksheets.value) {
|
|
123
|
+
console.log(` Sheet: ${sheet.name}`);
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const usedRange = await client
|
|
127
|
+
.api(
|
|
128
|
+
`/drives/${driveId}/items/${dataStoryFile.id}/workbook/worksheets/${sheet.id}/usedRange`
|
|
129
|
+
)
|
|
130
|
+
.get();
|
|
131
|
+
|
|
132
|
+
const values = usedRange.values || [];
|
|
133
|
+
|
|
134
|
+
if (values.length === 0) {
|
|
135
|
+
console.log(` (empty sheet, skipping)`);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const sheetName = sanitizeFilename(sheet.name);
|
|
140
|
+
const storyDir = join(STORIES_DIR, sheetName, 'data');
|
|
141
|
+
mkdirSync(storyDir, { recursive: true });
|
|
142
|
+
|
|
143
|
+
const json = convertToStoryJson(values);
|
|
144
|
+
const filepath = join(storyDir, 'copy.json');
|
|
145
|
+
writeFileSync(filepath, JSON.stringify(json, null, '\t'), 'utf8');
|
|
146
|
+
|
|
147
|
+
console.log(` -> ${filepath}`);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
console.error(` Error reading sheet: ${error.message}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
console.log('\nDone!');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
main().catch(console.error);
|