@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 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,6 @@
1
+ # Microsoft Graph / SharePoint Integration
2
+ # Get these values from Azure Portal > App registrations
3
+
4
+ tenantId=your-tenant-id
5
+ clientId=your-client-id
6
+ clientSecret=your-client-secret
@@ -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);