microsoft-onedrive-mock 1.0.0
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/.ENV_EXAMPLE +16 -0
- package/.github/workflows/ci.yml +41 -0
- package/.github/workflows/release.yml +50 -0
- package/README.md +1 -0
- package/dist/batch.d.ts +2 -0
- package/dist/batch.js +93 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +115 -0
- package/dist/routes/v1.d.ts +1 -0
- package/dist/routes/v1.js +201 -0
- package/dist/store.d.ts +24 -0
- package/dist/store.js +175 -0
- package/dist/types.d.ts +60 -0
- package/dist/types.js +2 -0
- package/eslint.config.mjs +15 -0
- package/examples/device-login.ts +84 -0
- package/examples/microsoft-login.html +227 -0
- package/examples/serve-login.ts +11 -0
- package/package.json +48 -0
- package/scripts/check-token.ts +70 -0
- package/specs/odata.xml +2 -0
- package/specs/openapi.yaml +196491 -0
- package/src/batch.ts +91 -0
- package/src/index.ts +102 -0
- package/src/routes/v1.ts +227 -0
- package/src/store.ts +191 -0
- package/src/types.ts +59 -0
- package/test/basics.test.ts +119 -0
- package/test/batch.test.ts +75 -0
- package/test/config.ts +63 -0
- package/test/etag.test.ts +69 -0
- package/test/search.test.ts +57 -0
- package/test/select.test.ts +68 -0
- package/tsconfig.json +19 -0
- package/vitest.config.ts +33 -0
package/.ENV_EXAMPLE
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# To run tests against the real Microsoft Graph API
|
|
2
|
+
# Copy this file to .ENV and fill in the values
|
|
3
|
+
|
|
4
|
+
# 'mock' (default) or 'real'
|
|
5
|
+
TEST_TARGET=real
|
|
6
|
+
|
|
7
|
+
# OAuth2 Access Token for your Microsoft Account
|
|
8
|
+
# Get one via 'npm run example:login'
|
|
9
|
+
ONEDRIVE_TOKEN=...
|
|
10
|
+
|
|
11
|
+
# Optional: Simulate latency in ms (only works with TEST_TARGET=mock)
|
|
12
|
+
# LATENCY=50
|
|
13
|
+
|
|
14
|
+
# Client ID for the Microsoft Login Example
|
|
15
|
+
ONEDRIVE_CLIENT_ID=e5...
|
|
16
|
+
ONEDRIVE_TENANT_ID=common
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ main, master ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ main, master ]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build-and-test:
|
|
11
|
+
runs-on: ubuntu-24.04
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- name: Use Node.js
|
|
17
|
+
uses: actions/setup-node@v4
|
|
18
|
+
with:
|
|
19
|
+
node-version: '24'
|
|
20
|
+
cache: 'npm'
|
|
21
|
+
|
|
22
|
+
- name: Install dependencies
|
|
23
|
+
run: npm install
|
|
24
|
+
|
|
25
|
+
- name: Lint
|
|
26
|
+
run: npm run lint
|
|
27
|
+
|
|
28
|
+
- name: Build
|
|
29
|
+
run: npm run build
|
|
30
|
+
|
|
31
|
+
- name: Test (Node)
|
|
32
|
+
run: npm test
|
|
33
|
+
|
|
34
|
+
- name: Install Playwright Browsers
|
|
35
|
+
run: npx playwright@1.58.0 install chromium
|
|
36
|
+
|
|
37
|
+
- name: Test (Browser)
|
|
38
|
+
run: npm run test:browser
|
|
39
|
+
|
|
40
|
+
- name: Test Slow
|
|
41
|
+
run: npm run test:slow
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
inputs:
|
|
6
|
+
version:
|
|
7
|
+
description: 'New Version (e.g. 1.0.0, or leave empty for patch)'
|
|
8
|
+
required: false
|
|
9
|
+
type: string
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build:
|
|
13
|
+
runs-on: ubuntu-24.04
|
|
14
|
+
# @link https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs
|
|
15
|
+
permissions:
|
|
16
|
+
contents: write
|
|
17
|
+
# @link https://docs.npmjs.com/generating-provenance-statements#about-npm-provenance
|
|
18
|
+
id-token: write
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
- uses: actions/setup-node@v4
|
|
22
|
+
with:
|
|
23
|
+
node-version: '24'
|
|
24
|
+
registry-url: 'https://registry.npmjs.org'
|
|
25
|
+
cache: 'npm'
|
|
26
|
+
|
|
27
|
+
- run: npm install
|
|
28
|
+
- name: Bump Version
|
|
29
|
+
id: bump
|
|
30
|
+
run: |
|
|
31
|
+
if [ -z "${{ inputs.version }}" ]; then
|
|
32
|
+
VERSION=$(npm version patch --no-git-tag-version)
|
|
33
|
+
else
|
|
34
|
+
VERSION=$(npm version ${{ inputs.version }} --no-git-tag-version)
|
|
35
|
+
fi
|
|
36
|
+
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
|
37
|
+
- run: npm run build
|
|
38
|
+
- run: npm run lint
|
|
39
|
+
- run: npm test
|
|
40
|
+
- run: npm publish
|
|
41
|
+
env:
|
|
42
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
43
|
+
|
|
44
|
+
- name: Push changes
|
|
45
|
+
run: |
|
|
46
|
+
git config --global user.name 'github-actions[bot]'
|
|
47
|
+
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
|
|
48
|
+
git add package.json package-lock.json
|
|
49
|
+
git commit -m "release: ${{ steps.bump.outputs.version }}"
|
|
50
|
+
git push
|
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# microsoft-onedrive-mock
|
package/dist/batch.d.ts
ADDED
package/dist/batch.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.handleBatchRequest = void 0;
|
|
13
|
+
const index_1 = require("./index");
|
|
14
|
+
const handleBatchRequest = (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
15
|
+
try {
|
|
16
|
+
const body = req.body;
|
|
17
|
+
if (!body || !Array.isArray(body.requests)) {
|
|
18
|
+
res.status(400).json({ error: { message: "Invalid batch payload" } });
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
// We use an internal app instance without auth/latency middleware to process routes cleanly
|
|
22
|
+
const internalApp = (0, index_1.createApp)({ serverLagBefore: 0, serverLagAfter: 0 });
|
|
23
|
+
const batchResponses = yield Promise.all(body.requests.map((part) => __awaiter(void 0, void 0, void 0, function* () {
|
|
24
|
+
const { id, method, url, body: partBody, headers: partHeaders } = part;
|
|
25
|
+
// Reconstruct path. Graph batches often send relative urls like /me/drive/root
|
|
26
|
+
// We append /v1.0/ to map to our internal routes if missing
|
|
27
|
+
const requestPath = url.startsWith('/v1.0') ? url : `/v1.0${url.startsWith('/') ? '' : '/'}${url}`;
|
|
28
|
+
return new Promise((resolve) => {
|
|
29
|
+
const simulatedReq = {
|
|
30
|
+
method: method || 'GET',
|
|
31
|
+
url: requestPath,
|
|
32
|
+
headers: partHeaders || {},
|
|
33
|
+
body: partBody,
|
|
34
|
+
query: {}
|
|
35
|
+
};
|
|
36
|
+
// Inject auth header from primary request to pass auth middleware
|
|
37
|
+
if (req.headers.authorization && !simulatedReq.headers.authorization) {
|
|
38
|
+
simulatedReq.headers.authorization = req.headers.authorization;
|
|
39
|
+
}
|
|
40
|
+
const simulatedRes = {
|
|
41
|
+
statusCode: 200,
|
|
42
|
+
headers: {},
|
|
43
|
+
charset: 'utf-8',
|
|
44
|
+
status: function (code) {
|
|
45
|
+
this.statusCode = code;
|
|
46
|
+
return this;
|
|
47
|
+
},
|
|
48
|
+
set: function (headerKey, headerValue) {
|
|
49
|
+
this.headers[headerKey] = headerValue;
|
|
50
|
+
return this;
|
|
51
|
+
},
|
|
52
|
+
setHeader: function (headerKey, headerValue) {
|
|
53
|
+
this.headers[headerKey] = headerValue;
|
|
54
|
+
return this;
|
|
55
|
+
},
|
|
56
|
+
json: function (data) {
|
|
57
|
+
this.data = data;
|
|
58
|
+
this.end();
|
|
59
|
+
},
|
|
60
|
+
send: function (data) {
|
|
61
|
+
// Normally this would be string/buffer, but let's just hold it
|
|
62
|
+
this.data = data;
|
|
63
|
+
this.end();
|
|
64
|
+
},
|
|
65
|
+
end: function () {
|
|
66
|
+
resolve({
|
|
67
|
+
id,
|
|
68
|
+
status: this.statusCode,
|
|
69
|
+
headers: this.headers,
|
|
70
|
+
body: this.data
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
// Bypass async handlers and directly feed to internal instance
|
|
75
|
+
internalApp(simulatedReq, simulatedRes, () => {
|
|
76
|
+
// Fallback next
|
|
77
|
+
resolve({
|
|
78
|
+
id,
|
|
79
|
+
status: 404,
|
|
80
|
+
body: { error: { message: "Not found within batch router" } }
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
})));
|
|
85
|
+
res.json({
|
|
86
|
+
responses: batchResponses
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
res.status(500).json({ error: { message: "Internal server error during batch", details: error.message } });
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
exports.handleBatchRequest = handleBatchRequest;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { driveStore } from './store';
|
|
2
|
+
import { AppConfig } from './types';
|
|
3
|
+
export * from './types';
|
|
4
|
+
declare const createApp: (config?: AppConfig) => import("express-serve-static-core").Express;
|
|
5
|
+
declare const startServer: (port: number, host?: string, config?: AppConfig) => import("node:http").Server<typeof import("node:http").IncomingMessage, typeof import("node:http").ServerResponse>;
|
|
6
|
+
export { createApp, startServer, driveStore };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
17
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
18
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
19
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
20
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
21
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
22
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
|
+
};
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.driveStore = exports.startServer = exports.createApp = void 0;
|
|
30
|
+
const express_1 = __importDefault(require("express"));
|
|
31
|
+
const cors_1 = __importDefault(require("cors"));
|
|
32
|
+
const store_1 = require("./store");
|
|
33
|
+
Object.defineProperty(exports, "driveStore", { enumerable: true, get: function () { return store_1.driveStore; } });
|
|
34
|
+
const v1_1 = require("./routes/v1");
|
|
35
|
+
const batch_1 = require("./batch");
|
|
36
|
+
__exportStar(require("./types"), exports);
|
|
37
|
+
const createApp = (config = {}) => {
|
|
38
|
+
if (!config.apiEndpoint) {
|
|
39
|
+
config.apiEndpoint = "";
|
|
40
|
+
}
|
|
41
|
+
const app = (0, express_1.default)();
|
|
42
|
+
app.use((0, cors_1.default)({
|
|
43
|
+
// For downloads Microsoft uses specific headers sometimes, expose ETag
|
|
44
|
+
exposedHeaders: ['ETag', 'Date', 'Content-Length', 'Location']
|
|
45
|
+
}));
|
|
46
|
+
app.set('etag', false);
|
|
47
|
+
// Latency simulator
|
|
48
|
+
app.use((req, res, next) => __awaiter(void 0, void 0, void 0, function* () {
|
|
49
|
+
const delay = Math.floor(Math.random() * 21);
|
|
50
|
+
if (delay > 0) {
|
|
51
|
+
yield new Promise(resolve => setTimeout(resolve, delay));
|
|
52
|
+
}
|
|
53
|
+
next();
|
|
54
|
+
}));
|
|
55
|
+
app.use(express_1.default.json({
|
|
56
|
+
verify: (req, res, buf) => {
|
|
57
|
+
req.rawBody = buf;
|
|
58
|
+
}
|
|
59
|
+
}));
|
|
60
|
+
app.use(express_1.default.text({
|
|
61
|
+
type: ['multipart/mixed', 'multipart/related', 'text/*', 'application/xml', 'application/octet-stream'],
|
|
62
|
+
verify: (req, res, buf) => {
|
|
63
|
+
req.rawBody = buf;
|
|
64
|
+
}
|
|
65
|
+
}));
|
|
66
|
+
// Explicit raw body for binary uploads
|
|
67
|
+
app.use(express_1.default.raw({
|
|
68
|
+
type: '*/*',
|
|
69
|
+
limit: '50mb',
|
|
70
|
+
verify: (req, res, buf) => {
|
|
71
|
+
req.rawBody = buf;
|
|
72
|
+
}
|
|
73
|
+
}));
|
|
74
|
+
// Batch Route
|
|
75
|
+
app.post('/v1.0/$batch', batch_1.handleBatchRequest);
|
|
76
|
+
// Debug
|
|
77
|
+
app.post('/debug/clear', (req, res) => {
|
|
78
|
+
store_1.driveStore.clear();
|
|
79
|
+
res.status(200).send('Cleared');
|
|
80
|
+
});
|
|
81
|
+
// Health Check
|
|
82
|
+
app.get('/', (req, res) => {
|
|
83
|
+
res.status(200).send('OK');
|
|
84
|
+
});
|
|
85
|
+
// Auth Middleware
|
|
86
|
+
const validTokens = ['valid-token', 'another-valid-token'];
|
|
87
|
+
app.use((req, res, next) => {
|
|
88
|
+
const authHeaderVal = req.headers.authorization;
|
|
89
|
+
const authHeader = Array.isArray(authHeaderVal) ? authHeaderVal[0] : authHeaderVal;
|
|
90
|
+
if (!authHeader) {
|
|
91
|
+
res.status(401).json({ error: { code: "unauthenticated", message: "Unauthorized: No token provided" } });
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const token = authHeader.split(' ')[1];
|
|
95
|
+
if (!validTokens.includes(token)) {
|
|
96
|
+
res.status(401).json({ error: { code: "unauthenticated", message: "Unauthorized: Invalid token" } });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
next();
|
|
100
|
+
});
|
|
101
|
+
app.use((0, v1_1.createV1Router)());
|
|
102
|
+
return app;
|
|
103
|
+
};
|
|
104
|
+
exports.createApp = createApp;
|
|
105
|
+
const startServer = (port, host = 'localhost', config = {}) => {
|
|
106
|
+
const app = createApp(config);
|
|
107
|
+
return app.listen(port, host, () => {
|
|
108
|
+
console.log(`Server is running on http://${host}:${port}`);
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
exports.startServer = startServer;
|
|
112
|
+
if (require.main === module) {
|
|
113
|
+
const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3006;
|
|
114
|
+
startServer(port);
|
|
115
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const createV1Router: () => import("express-serve-static-core").Router;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createV1Router = void 0;
|
|
7
|
+
const express_1 = __importDefault(require("express"));
|
|
8
|
+
const store_1 = require("../store");
|
|
9
|
+
const createV1Router = () => {
|
|
10
|
+
const app = express_1.default.Router();
|
|
11
|
+
// Helper to apply ?$select=id,name
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13
|
+
const applySelect = (item, selectQuery) => {
|
|
14
|
+
if (!selectQuery || typeof selectQuery !== 'string')
|
|
15
|
+
return item;
|
|
16
|
+
const fields = selectQuery.split(',').map(f => f.trim());
|
|
17
|
+
if (fields.length === 0)
|
|
18
|
+
return item;
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
|
+
const result = {};
|
|
21
|
+
for (const field of fields) {
|
|
22
|
+
if (item[field] !== undefined) {
|
|
23
|
+
result[field] = item[field];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
27
|
+
};
|
|
28
|
+
// GET /me/drive/root
|
|
29
|
+
app.get('/v1.0/me/drive/root', (req, res) => {
|
|
30
|
+
const root = store_1.driveStore.getItem('root');
|
|
31
|
+
if (!root)
|
|
32
|
+
return res.status(404).json({ error: { message: "Root not found" } });
|
|
33
|
+
res.json(applySelect(root, req.query.$select));
|
|
34
|
+
});
|
|
35
|
+
// GET /me/drive/items/{id}
|
|
36
|
+
app.get('/v1.0/me/drive/items/:itemId', (req, res) => {
|
|
37
|
+
let itemId = req.params.itemId;
|
|
38
|
+
if (itemId === 'root')
|
|
39
|
+
itemId = 'root'; // Already handled if just mapping
|
|
40
|
+
const item = store_1.driveStore.getItem(itemId);
|
|
41
|
+
if (!item) {
|
|
42
|
+
res.status(404).json({ error: { code: "itemNotFound", message: "Item not found" } });
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
res.json(applySelect(item, req.query.$select));
|
|
46
|
+
});
|
|
47
|
+
// GET /me/drive/items/{id}/children
|
|
48
|
+
app.get('/v1.0/me/drive/items/:itemId/children', (req, res) => {
|
|
49
|
+
const itemId = req.params.itemId;
|
|
50
|
+
const children = store_1.driveStore.listItems(itemId);
|
|
51
|
+
const mappedChildren = children.map(c => applySelect(c, req.query.$select));
|
|
52
|
+
res.json({ value: mappedChildren });
|
|
53
|
+
});
|
|
54
|
+
// GET /me/drive/root/search(q='query')
|
|
55
|
+
app.get('/v1.0/me/drive/root/search\\(q=\':query\'\\)', (req, res) => {
|
|
56
|
+
let query = req.params.query || "";
|
|
57
|
+
// Decode URI component incase it's URL encoded like %20 or %27
|
|
58
|
+
query = decodeURIComponent(query);
|
|
59
|
+
// Strip out the trailing quote matching if it caught the literal '
|
|
60
|
+
if (query.endsWith("'"))
|
|
61
|
+
query = query.slice(0, -1);
|
|
62
|
+
query = query.toLowerCase();
|
|
63
|
+
// recursively find items
|
|
64
|
+
const allItems = store_1.driveStore.getAllItems();
|
|
65
|
+
const results = allItems.filter(item => item.name.toLowerCase().includes(query) && item.id !== 'root');
|
|
66
|
+
const mappedResults = results.map(c => applySelect(c, req.query.$select));
|
|
67
|
+
res.json({ value: mappedResults });
|
|
68
|
+
});
|
|
69
|
+
// POST /me/drive/items/{parent-id}/children (Create Metadata / Folder)
|
|
70
|
+
app.post('/v1.0/me/drive/items/:itemId/children', (req, res) => {
|
|
71
|
+
const parentId = req.params.itemId;
|
|
72
|
+
const body = req.body || {};
|
|
73
|
+
let parentItem = store_1.driveStore.getItem(parentId);
|
|
74
|
+
if (!parentItem && parentId === 'root') {
|
|
75
|
+
parentItem = store_1.driveStore.createItem({ id: 'root', name: 'root' }, true);
|
|
76
|
+
}
|
|
77
|
+
else if (!parentItem) {
|
|
78
|
+
res.status(404).json({ error: { code: "itemNotFound", message: "Parent not found" } });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const isFolder = !!body.folder;
|
|
82
|
+
const newItem = store_1.driveStore.createItem(Object.assign({ name: body.name || "Untitled", parentReference: { id: parentId } }, body), isFolder);
|
|
83
|
+
res.status(201).json(applySelect(newItem, req.query.$select));
|
|
84
|
+
});
|
|
85
|
+
// DELETE /me/drive/items/{id}
|
|
86
|
+
app.delete('/v1.0/me/drive/items/:itemId', (req, res) => {
|
|
87
|
+
const fileId = req.params.itemId;
|
|
88
|
+
if (fileId === 'root') {
|
|
89
|
+
res.status(400).json({ error: { message: "Cannot delete root" } });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const item = store_1.driveStore.getItem(fileId);
|
|
93
|
+
if (!item) {
|
|
94
|
+
res.status(404).json({ error: { code: "itemNotFound", message: "Item not found" } });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const ifMatch = req.header('If-Match');
|
|
98
|
+
if (ifMatch && ifMatch !== item.eTag) {
|
|
99
|
+
res.status(412).json({ error: { code: "PreconditionFailed", message: "ETag mismatch" } });
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
store_1.driveStore.deleteItem(fileId);
|
|
103
|
+
res.status(204).send();
|
|
104
|
+
});
|
|
105
|
+
// GET /me/drive/items/{id}/content
|
|
106
|
+
app.get('/v1.0/me/drive/items/:itemId/content', (req, res) => {
|
|
107
|
+
var _a;
|
|
108
|
+
const fileId = req.params.itemId;
|
|
109
|
+
const file = store_1.driveStore.getItem(fileId);
|
|
110
|
+
if (!file || file.folder) {
|
|
111
|
+
res.status(404).json({ error: { code: "itemNotFound", message: "File not found" } });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if ((_a = file.file) === null || _a === void 0 ? void 0 : _a.mimeType) {
|
|
115
|
+
res.setHeader('Content-Type', file.file.mimeType);
|
|
116
|
+
}
|
|
117
|
+
if (file.content === undefined) {
|
|
118
|
+
res.send("");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (Buffer.isBuffer(file.content)) {
|
|
122
|
+
res.send(file.content);
|
|
123
|
+
}
|
|
124
|
+
else if (typeof file.content === 'object') {
|
|
125
|
+
res.json(file.content);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
res.send(file.content);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
// PUT /me/drive/items/{parent-id}:/{filename}:/content
|
|
132
|
+
app.put('/v1.0/me/drive/items/:parentId\\:/:filename\\:/content', (req, res) => {
|
|
133
|
+
const parentId = req.params.parentId;
|
|
134
|
+
const filename = req.params.filename;
|
|
135
|
+
// Find existing or create
|
|
136
|
+
const children = store_1.driveStore.listItems(parentId);
|
|
137
|
+
let item = children.find(c => c.name === filename);
|
|
138
|
+
const content = req.rawBody !== undefined ? req.rawBody : req.body;
|
|
139
|
+
const headerMime = req.headers['content-type'];
|
|
140
|
+
const mimeType = (Array.isArray(headerMime) ? headerMime[0] : headerMime) || 'application/octet-stream';
|
|
141
|
+
const isNew = !item;
|
|
142
|
+
if (item) {
|
|
143
|
+
const ifMatch = req.header('If-Match');
|
|
144
|
+
if (ifMatch && ifMatch !== item.eTag) {
|
|
145
|
+
res.status(412).json({ error: { code: "PreconditionFailed", message: "ETag mismatch" } });
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
// Update
|
|
149
|
+
item = store_1.driveStore.updateItem(item.id, { content, file: { mimeType } });
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
// Create
|
|
153
|
+
item = store_1.driveStore.createItem({
|
|
154
|
+
name: filename,
|
|
155
|
+
parentReference: { id: parentId },
|
|
156
|
+
content,
|
|
157
|
+
file: { mimeType }
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
res.status(isNew ? 201 : 200).json(applySelect(item, req.query.$select));
|
|
161
|
+
});
|
|
162
|
+
// PUT /me/drive/items/{id}/content
|
|
163
|
+
app.put('/v1.0/me/drive/items/:itemId/content', (req, res) => {
|
|
164
|
+
var _a;
|
|
165
|
+
const itemId = req.params.itemId;
|
|
166
|
+
let item = store_1.driveStore.getItem(itemId);
|
|
167
|
+
if (!item || item.folder) {
|
|
168
|
+
res.status(404).json({ error: { code: "itemNotFound", message: "Item not found" } });
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const ifMatch = req.header('If-Match');
|
|
172
|
+
if (ifMatch && ifMatch !== item.eTag) {
|
|
173
|
+
res.status(412).json({ error: { code: "PreconditionFailed", message: "ETag mismatch" } });
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const content = req.rawBody !== undefined ? req.rawBody : req.body;
|
|
177
|
+
const headerMime = req.headers['content-type'];
|
|
178
|
+
const mimeType = (Array.isArray(headerMime) ? headerMime[0] : headerMime) || ((_a = item.file) === null || _a === void 0 ? void 0 : _a.mimeType) || 'application/octet-stream';
|
|
179
|
+
item = store_1.driveStore.updateItem(item.id, { content, file: { mimeType } });
|
|
180
|
+
res.status(200).json(applySelect(item, req.query.$select));
|
|
181
|
+
});
|
|
182
|
+
// Delta Query
|
|
183
|
+
// GET /me/drive/root/delta
|
|
184
|
+
app.get('/v1.0/me/drive/root/delta', (req, res) => {
|
|
185
|
+
const tokenStr = req.query.token;
|
|
186
|
+
let token = undefined;
|
|
187
|
+
if (tokenStr)
|
|
188
|
+
token = tokenStr;
|
|
189
|
+
const result = store_1.driveStore.getDelta(token);
|
|
190
|
+
const host = req.headers.host || 'localhost';
|
|
191
|
+
const protocol = req.protocol || 'http';
|
|
192
|
+
const baseUrl = `${protocol}://${host}`;
|
|
193
|
+
res.json({
|
|
194
|
+
'@odata.context': `${baseUrl}/v1.0/$metadata#Collection(driveItem)`,
|
|
195
|
+
'@odata.deltaLink': `${baseUrl}/v1.0/me/drive/root/delta?token=${result.deltaLink}`,
|
|
196
|
+
value: result.items
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
return app;
|
|
200
|
+
};
|
|
201
|
+
exports.createV1Router = createV1Router;
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { DriveItem } from './types';
|
|
2
|
+
export declare class DriveStore {
|
|
3
|
+
private items;
|
|
4
|
+
private deltaHistory;
|
|
5
|
+
constructor();
|
|
6
|
+
private calculateStats;
|
|
7
|
+
createItem(item: Partial<DriveItem> & {
|
|
8
|
+
name: string;
|
|
9
|
+
}, isFolder?: boolean): DriveItem;
|
|
10
|
+
updateItem(id: string, updates: Partial<DriveItem>): DriveItem | null;
|
|
11
|
+
getItem(id: string): DriveItem | null;
|
|
12
|
+
deleteItem(id: string): boolean;
|
|
13
|
+
listItems(parentId?: string): DriveItem[];
|
|
14
|
+
getAllItems(): DriveItem[];
|
|
15
|
+
clear(): void;
|
|
16
|
+
private addDeltaHistory;
|
|
17
|
+
getDeltaToken(): string;
|
|
18
|
+
getDelta(token?: string): {
|
|
19
|
+
items: DriveItem[];
|
|
20
|
+
deltaLink: string;
|
|
21
|
+
};
|
|
22
|
+
private incrementParentChildCount;
|
|
23
|
+
}
|
|
24
|
+
export declare const driveStore: DriveStore;
|