google-drive-mock 1.0.13 → 1.1.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/.agent/rules/project-guidelines.md +5 -0
- package/.github/workflows/release.yml +12 -4
- package/dist/batch.js +1 -1
- package/dist/index.js +1 -1
- package/dist/mappers.d.ts +8 -0
- package/dist/mappers.js +79 -0
- package/dist/routes/v3.js +68 -6
- package/dist/store.js +1 -1
- package/package.json +1 -1
- package/src/batch.ts +2 -1
- package/src/index.ts +1 -1
- package/src/mappers.ts +84 -0
- package/src/routes/v3.ts +71 -5
- package/src/store.ts +1 -1
- package/test/batch_insert_download.test.ts +150 -0
- package/test/concurrent_fetch.test.ts +17 -10
- package/test/dates_and_sorting.test.ts +0 -2
- package/test/folder_query.test.ts +177 -0
- package/test/iterate_changes.test.ts +920 -0
- package/test/parallel_update.test.ts +138 -0
- package/test/url_parameters.test.ts +76 -0
|
@@ -18,6 +18,11 @@ When working on this project, always adhere to the following workflow to ensure
|
|
|
18
18
|
- Run this **AFTER** verifying the tests against the Real API.
|
|
19
19
|
- This ensures that the Mock server implementation correctly handles the now-verified tests.
|
|
20
20
|
|
|
21
|
+
4. **Ensure build works**
|
|
22
|
+
- Run "npm run lint"
|
|
23
|
+
- Run "npm run build"
|
|
24
|
+
|
|
25
|
+
|
|
21
26
|
|
|
22
27
|
No Goes:
|
|
23
28
|
|
|
@@ -4,8 +4,8 @@ on:
|
|
|
4
4
|
workflow_dispatch:
|
|
5
5
|
inputs:
|
|
6
6
|
version:
|
|
7
|
-
description: 'New Version (e.g. 1.0.0)'
|
|
8
|
-
required:
|
|
7
|
+
description: 'New Version (e.g. 1.0.0, or leave empty for patch)'
|
|
8
|
+
required: false
|
|
9
9
|
type: string
|
|
10
10
|
|
|
11
11
|
jobs:
|
|
@@ -25,7 +25,15 @@ jobs:
|
|
|
25
25
|
cache: 'npm'
|
|
26
26
|
|
|
27
27
|
- run: npm install
|
|
28
|
-
-
|
|
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
|
|
29
37
|
- run: npm run build
|
|
30
38
|
- run: npm run lint
|
|
31
39
|
- run: npm test
|
|
@@ -38,5 +46,5 @@ jobs:
|
|
|
38
46
|
git config --global user.name 'github-actions[bot]'
|
|
39
47
|
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
|
|
40
48
|
git add package.json package-lock.json
|
|
41
|
-
git commit -m "release: ${{
|
|
49
|
+
git commit -m "release: ${{ steps.bump.outputs.version }}"
|
|
42
50
|
git push
|
package/dist/batch.js
CHANGED
|
@@ -100,7 +100,7 @@ function processPart(part, req) {
|
|
|
100
100
|
body: Object.assign({ kind: "drive#about" }, about)
|
|
101
101
|
};
|
|
102
102
|
}
|
|
103
|
-
// POST Create File
|
|
103
|
+
// POST Create File (Standard)
|
|
104
104
|
if (part.method === 'POST' && filesListMatch) {
|
|
105
105
|
if (!part.body || !part.body.name) {
|
|
106
106
|
return { contentId: part.contentId, statusCode: 400, body: { error: { code: 400, message: 'Name required' } } };
|
package/dist/index.js
CHANGED
|
@@ -26,7 +26,7 @@ const createApp = (config = {}) => {
|
|
|
26
26
|
}
|
|
27
27
|
const app = (0, express_1.default)();
|
|
28
28
|
app.use((0, cors_1.default)({
|
|
29
|
-
exposedHeaders: ['ETag']
|
|
29
|
+
exposedHeaders: ['ETag', 'Date', 'Content-Length']
|
|
30
30
|
}));
|
|
31
31
|
app.set('etag', false); // Disable default ETag generation to match Real API behavior
|
|
32
32
|
app.use((req, res, next) => __awaiter(void 0, void 0, void 0, function* () {
|
package/dist/mappers.d.ts
CHANGED
|
@@ -7,3 +7,11 @@ export declare function toV2File(file: DriveFile): Record<string, unknown>;
|
|
|
7
7
|
* Maps a V2 API File Update/Insert body to a partial Internal DriveFile (V3 format).
|
|
8
8
|
*/
|
|
9
9
|
export declare function fromV2Update(body: Record<string, unknown>): Partial<DriveFile>;
|
|
10
|
+
/**
|
|
11
|
+
* Filters an object based on the Google Drive API `fields` parameter.
|
|
12
|
+
* Supports nested fields like "files(id,name,parents)".
|
|
13
|
+
*
|
|
14
|
+
* @param data The object to filter
|
|
15
|
+
* @param fields The fields selection string
|
|
16
|
+
*/
|
|
17
|
+
export declare function applyFields(data: unknown, fields: string): unknown;
|
package/dist/mappers.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.toV2File = toV2File;
|
|
4
4
|
exports.fromV2Update = fromV2Update;
|
|
5
|
+
exports.applyFields = applyFields;
|
|
5
6
|
/**
|
|
6
7
|
* Maps an internal DriveFile (V3 format) to a V2 API File resource.
|
|
7
8
|
*/
|
|
@@ -61,3 +62,81 @@ function fromV2Update(body) {
|
|
|
61
62
|
}
|
|
62
63
|
return update;
|
|
63
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Filters an object based on the Google Drive API `fields` parameter.
|
|
67
|
+
* Supports nested fields like "files(id,name,parents)".
|
|
68
|
+
*
|
|
69
|
+
* @param data The object to filter
|
|
70
|
+
* @param fields The fields selection string
|
|
71
|
+
*/
|
|
72
|
+
function applyFields(data, fields) {
|
|
73
|
+
if (!fields || fields === '*')
|
|
74
|
+
return data;
|
|
75
|
+
// Parse top-level fields
|
|
76
|
+
const parsedFields = [];
|
|
77
|
+
let depth = 0;
|
|
78
|
+
let currentStart = 0;
|
|
79
|
+
for (let i = 0; i < fields.length; i++) {
|
|
80
|
+
const char = fields[i];
|
|
81
|
+
if (char === '(')
|
|
82
|
+
depth++;
|
|
83
|
+
else if (char === ')')
|
|
84
|
+
depth--;
|
|
85
|
+
else if (char === ',' && depth === 0) {
|
|
86
|
+
// Split
|
|
87
|
+
parseFieldPart(fields.substring(currentStart, i), parsedFields);
|
|
88
|
+
currentStart = i + 1;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Last part
|
|
92
|
+
parseFieldPart(fields.substring(currentStart), parsedFields);
|
|
93
|
+
if (Array.isArray(data)) {
|
|
94
|
+
// If data is array, apply to each item?
|
|
95
|
+
// Usually fields selection on array applies to the object WRAPPING the array,
|
|
96
|
+
// e.g. "files(id)" on { files: [...] }.
|
|
97
|
+
// But if we are called RESURSIVELY on an array value, we should map it.
|
|
98
|
+
return data.map(item => applyFields(item, fields));
|
|
99
|
+
}
|
|
100
|
+
if (typeof data !== 'object' || data === null) {
|
|
101
|
+
return data;
|
|
102
|
+
}
|
|
103
|
+
const dataObj = data;
|
|
104
|
+
const result = {};
|
|
105
|
+
for (const field of parsedFields) {
|
|
106
|
+
if (field.key === '*') {
|
|
107
|
+
// Wildcard at this level - copy everything?
|
|
108
|
+
// Logic can be complex. For now, strict parity with requested use case.
|
|
109
|
+
return data;
|
|
110
|
+
}
|
|
111
|
+
if (Object.prototype.hasOwnProperty.call(dataObj, field.key)) {
|
|
112
|
+
const value = dataObj[field.key];
|
|
113
|
+
if (field.subFields) {
|
|
114
|
+
// Recursive apply
|
|
115
|
+
if (Array.isArray(value)) {
|
|
116
|
+
result[field.key] = value.map((item) => applyFields(item, field.subFields));
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
result[field.key] = applyFields(value, field.subFields);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
result[field.key] = value;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
function parseFieldPart(part, result) {
|
|
130
|
+
const trimmed = part.trim();
|
|
131
|
+
if (!trimmed)
|
|
132
|
+
return;
|
|
133
|
+
const parenStart = trimmed.indexOf('(');
|
|
134
|
+
if (parenStart !== -1 && trimmed.endsWith(')')) {
|
|
135
|
+
const key = trimmed.substring(0, parenStart);
|
|
136
|
+
const subFields = trimmed.substring(parenStart + 1, trimmed.length - 1);
|
|
137
|
+
result.push({ key, subFields });
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
result.push({ key: trimmed });
|
|
141
|
+
}
|
|
142
|
+
}
|
package/dist/routes/v3.js
CHANGED
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.createV3Router = void 0;
|
|
7
7
|
const express_1 = __importDefault(require("express"));
|
|
8
8
|
const store_1 = require("../store");
|
|
9
|
+
const mappers_1 = require("../mappers");
|
|
9
10
|
const createV3Router = () => {
|
|
10
11
|
const app = express_1.default.Router();
|
|
11
12
|
// About
|
|
@@ -22,7 +23,7 @@ const createV3Router = () => {
|
|
|
22
23
|
// Enhanced query parser for Mock
|
|
23
24
|
// Recursive function to handle nested OR/AND logic with parens
|
|
24
25
|
const evaluateQuery = (queryStr, file) => {
|
|
25
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
26
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
|
|
26
27
|
const str = queryStr.trim();
|
|
27
28
|
if (!str)
|
|
28
29
|
return true;
|
|
@@ -136,6 +137,14 @@ const createV3Router = () => {
|
|
|
136
137
|
const timeStr = (_k = part.match(/modifiedTime < '(.*)'/)) === null || _k === void 0 ? void 0 : _k[1];
|
|
137
138
|
return !!(timeStr && new Date(file.modifiedTime) < new Date(timeStr));
|
|
138
139
|
}
|
|
140
|
+
if (part.startsWith("modifiedTime = '")) {
|
|
141
|
+
const timeStr = (_l = part.match(/modifiedTime = '(.*)'/)) === null || _l === void 0 ? void 0 : _l[1];
|
|
142
|
+
return !!(timeStr && new Date(file.modifiedTime).toISOString() === new Date(timeStr).toISOString());
|
|
143
|
+
}
|
|
144
|
+
if (part.startsWith("modifiedTime >= '")) {
|
|
145
|
+
const timeStr = (_m = part.match(/modifiedTime >= '(.*)'/)) === null || _m === void 0 ? void 0 : _m[1];
|
|
146
|
+
return !!(timeStr && new Date(file.modifiedTime) >= new Date(timeStr));
|
|
147
|
+
}
|
|
139
148
|
// Fallback / Unknown
|
|
140
149
|
return true;
|
|
141
150
|
};
|
|
@@ -172,11 +181,41 @@ const createV3Router = () => {
|
|
|
172
181
|
return 0;
|
|
173
182
|
});
|
|
174
183
|
}
|
|
175
|
-
|
|
184
|
+
// Pagination
|
|
185
|
+
const pageSize = req.query.pageSize ? parseInt(req.query.pageSize, 10) : 100; // Default 100
|
|
186
|
+
let skip = 0;
|
|
187
|
+
if (req.query.pageToken) {
|
|
188
|
+
try {
|
|
189
|
+
const tokenJson = Buffer.from(req.query.pageToken, 'base64').toString('utf-8');
|
|
190
|
+
const tokenData = JSON.parse(tokenJson);
|
|
191
|
+
if (typeof tokenData.skip === 'number') {
|
|
192
|
+
skip = tokenData.skip;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
catch (_a) {
|
|
196
|
+
// Ignore invalid token, start from 0
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const totalFiles = files.length;
|
|
200
|
+
const resultFiles = files.slice(skip, skip + pageSize);
|
|
201
|
+
let nextPageToken;
|
|
202
|
+
if (skip + pageSize < totalFiles) {
|
|
203
|
+
const nextSkip = skip + pageSize;
|
|
204
|
+
nextPageToken = Buffer.from(JSON.stringify({ skip: nextSkip })).toString('base64');
|
|
205
|
+
}
|
|
206
|
+
const response = {
|
|
176
207
|
kind: "drive#fileList",
|
|
177
208
|
incompleteSearch: false,
|
|
178
|
-
files:
|
|
179
|
-
|
|
209
|
+
files: resultFiles,
|
|
210
|
+
nextPageToken
|
|
211
|
+
};
|
|
212
|
+
const fields = req.query.fields;
|
|
213
|
+
if (fields) {
|
|
214
|
+
res.json((0, mappers_1.applyFields)(response, fields));
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
res.json(response);
|
|
218
|
+
}
|
|
180
219
|
});
|
|
181
220
|
// Changes: Get Start Page Token
|
|
182
221
|
app.get('/drive/v3/changes/startPageToken', (req, res) => {
|
|
@@ -419,8 +458,10 @@ const createV3Router = () => {
|
|
|
419
458
|
res.status(400).send("Invalid file ID");
|
|
420
459
|
return;
|
|
421
460
|
}
|
|
422
|
-
const updates = req.body;
|
|
423
|
-
|
|
461
|
+
const updates = req.body || {};
|
|
462
|
+
const hasBody = Object.keys(updates).length > 0;
|
|
463
|
+
const hasQueryParams = req.query.addParents || req.query.removeParents;
|
|
464
|
+
if (!hasBody && !hasQueryParams) {
|
|
424
465
|
res.status(400).json({ error: { code: 400, message: "Bad Request: No updates provided" } });
|
|
425
466
|
return;
|
|
426
467
|
}
|
|
@@ -429,6 +470,27 @@ const createV3Router = () => {
|
|
|
429
470
|
res.status(404).json({ error: { code: 404, message: "File not found" } });
|
|
430
471
|
return;
|
|
431
472
|
}
|
|
473
|
+
const addParents = req.query.addParents;
|
|
474
|
+
if (addParents) {
|
|
475
|
+
const parentsToAdd = addParents.split(',');
|
|
476
|
+
const currentParents = updatedFile.parents || [];
|
|
477
|
+
const newParents = [...new Set([...currentParents, ...parentsToAdd])]; // Union
|
|
478
|
+
// Update the file with new parents
|
|
479
|
+
const result = store_1.driveStore.updateFile(fileId, { parents: newParents });
|
|
480
|
+
if (result) {
|
|
481
|
+
Object.assign(updatedFile, result);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
const removeParents = req.query.removeParents;
|
|
485
|
+
if (removeParents) {
|
|
486
|
+
const parentsToRemove = removeParents.split(',');
|
|
487
|
+
const currentParents = updatedFile.parents || [];
|
|
488
|
+
const newParents = currentParents.filter(p => !parentsToRemove.includes(p));
|
|
489
|
+
const result = store_1.driveStore.updateFile(fileId, { parents: newParents });
|
|
490
|
+
if (result) {
|
|
491
|
+
Object.assign(updatedFile, result);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
432
494
|
res.json(updatedFile);
|
|
433
495
|
});
|
|
434
496
|
// Files: Delete
|
package/dist/store.js
CHANGED
|
@@ -81,7 +81,7 @@ class DriveStore {
|
|
|
81
81
|
}
|
|
82
82
|
// Merge updates and increment version
|
|
83
83
|
const newVersion = file.version + 1;
|
|
84
|
-
const updatedFile = Object.assign(Object.assign(Object.assign(Object.assign({}, file), updates), statsUpdates), { version: newVersion, etag: String(newVersion), modifiedTime: new Date().toISOString() });
|
|
84
|
+
const updatedFile = Object.assign(Object.assign(Object.assign(Object.assign({}, file), updates), statsUpdates), { version: newVersion, etag: String(newVersion), modifiedTime: updates.modifiedTime || new Date().toISOString() });
|
|
85
85
|
this.files.set(id, updatedFile);
|
|
86
86
|
this.addChange(updatedFile);
|
|
87
87
|
return updatedFile;
|
package/package.json
CHANGED
package/src/batch.ts
CHANGED
|
@@ -133,7 +133,8 @@ function processPart(part: BatchPart, req: Request): BatchResponse {
|
|
|
133
133
|
};
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
|
|
136
|
+
|
|
137
|
+
// POST Create File (Standard)
|
|
137
138
|
if (part.method === 'POST' && filesListMatch) {
|
|
138
139
|
if (!part.body || !part.body.name) {
|
|
139
140
|
return { contentId: part.contentId, statusCode: 400, body: { error: { code: 400, message: 'Name required' } } };
|
package/src/index.ts
CHANGED
|
@@ -14,7 +14,7 @@ const createApp = (config: AppConfig = {}) => {
|
|
|
14
14
|
|
|
15
15
|
const app = express();
|
|
16
16
|
app.use(cors({
|
|
17
|
-
exposedHeaders: ['ETag']
|
|
17
|
+
exposedHeaders: ['ETag', 'Date', 'Content-Length']
|
|
18
18
|
}));
|
|
19
19
|
app.set('etag', false); // Disable default ETag generation to match Real API behavior
|
|
20
20
|
|
package/src/mappers.ts
CHANGED
|
@@ -59,3 +59,87 @@ export function fromV2Update(body: Record<string, unknown>): Partial<DriveFile>
|
|
|
59
59
|
|
|
60
60
|
return update;
|
|
61
61
|
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Filters an object based on the Google Drive API `fields` parameter.
|
|
66
|
+
* Supports nested fields like "files(id,name,parents)".
|
|
67
|
+
*
|
|
68
|
+
* @param data The object to filter
|
|
69
|
+
* @param fields The fields selection string
|
|
70
|
+
*/
|
|
71
|
+
export function applyFields(data: unknown, fields: string): unknown {
|
|
72
|
+
if (!fields || fields === '*') return data;
|
|
73
|
+
|
|
74
|
+
// Parse top-level fields
|
|
75
|
+
const parsedFields: { key: string, subFields?: string }[] = [];
|
|
76
|
+
|
|
77
|
+
let depth = 0;
|
|
78
|
+
let currentStart = 0;
|
|
79
|
+
|
|
80
|
+
for (let i = 0; i < fields.length; i++) {
|
|
81
|
+
const char = fields[i];
|
|
82
|
+
if (char === '(') depth++;
|
|
83
|
+
else if (char === ')') depth--;
|
|
84
|
+
else if (char === ',' && depth === 0) {
|
|
85
|
+
// Split
|
|
86
|
+
parseFieldPart(fields.substring(currentStart, i), parsedFields);
|
|
87
|
+
currentStart = i + 1;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Last part
|
|
91
|
+
parseFieldPart(fields.substring(currentStart), parsedFields);
|
|
92
|
+
|
|
93
|
+
if (Array.isArray(data)) {
|
|
94
|
+
// If data is array, apply to each item?
|
|
95
|
+
// Usually fields selection on array applies to the object WRAPPING the array,
|
|
96
|
+
// e.g. "files(id)" on { files: [...] }.
|
|
97
|
+
// But if we are called RESURSIVELY on an array value, we should map it.
|
|
98
|
+
return data.map(item => applyFields(item, fields));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (typeof data !== 'object' || data === null) {
|
|
102
|
+
return data;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const dataObj = data as Record<string, unknown>;
|
|
106
|
+
const result: Record<string, unknown> = {};
|
|
107
|
+
|
|
108
|
+
for (const field of parsedFields) {
|
|
109
|
+
if (field.key === '*') {
|
|
110
|
+
// Wildcard at this level - copy everything?
|
|
111
|
+
// Logic can be complex. For now, strict parity with requested use case.
|
|
112
|
+
return data;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (Object.prototype.hasOwnProperty.call(dataObj, field.key)) {
|
|
116
|
+
const value = dataObj[field.key];
|
|
117
|
+
if (field.subFields) {
|
|
118
|
+
// Recursive apply
|
|
119
|
+
if (Array.isArray(value)) {
|
|
120
|
+
result[field.key] = value.map((item: unknown) => applyFields(item, field.subFields!));
|
|
121
|
+
} else {
|
|
122
|
+
result[field.key] = applyFields(value, field.subFields!);
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
result[field.key] = value;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function parseFieldPart(part: string, result: { key: string, subFields?: string }[]) {
|
|
134
|
+
const trimmed = part.trim();
|
|
135
|
+
if (!trimmed) return;
|
|
136
|
+
|
|
137
|
+
const parenStart = trimmed.indexOf('(');
|
|
138
|
+
if (parenStart !== -1 && trimmed.endsWith(')')) {
|
|
139
|
+
const key = trimmed.substring(0, parenStart);
|
|
140
|
+
const subFields = trimmed.substring(parenStart + 1, trimmed.length - 1);
|
|
141
|
+
result.push({ key, subFields });
|
|
142
|
+
} else {
|
|
143
|
+
result.push({ key: trimmed });
|
|
144
|
+
}
|
|
145
|
+
}
|
package/src/routes/v3.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import express, { Request, Response } from 'express';
|
|
2
2
|
import { driveStore } from '../store';
|
|
3
|
+
import { applyFields } from '../mappers';
|
|
3
4
|
|
|
4
5
|
export const createV3Router = () => {
|
|
5
6
|
const app = express.Router();
|
|
@@ -136,6 +137,14 @@ export const createV3Router = () => {
|
|
|
136
137
|
const timeStr = part.match(/modifiedTime < '(.*)'/)?.[1];
|
|
137
138
|
return !!(timeStr && new Date(file.modifiedTime) < new Date(timeStr));
|
|
138
139
|
}
|
|
140
|
+
if (part.startsWith("modifiedTime = '")) {
|
|
141
|
+
const timeStr = part.match(/modifiedTime = '(.*)'/)?.[1];
|
|
142
|
+
return !!(timeStr && new Date(file.modifiedTime).toISOString() === new Date(timeStr).toISOString());
|
|
143
|
+
}
|
|
144
|
+
if (part.startsWith("modifiedTime >= '")) {
|
|
145
|
+
const timeStr = part.match(/modifiedTime >= '(.*)'/)?.[1];
|
|
146
|
+
return !!(timeStr && new Date(file.modifiedTime) >= new Date(timeStr));
|
|
147
|
+
}
|
|
139
148
|
|
|
140
149
|
// Fallback / Unknown
|
|
141
150
|
return true;
|
|
@@ -177,11 +186,43 @@ export const createV3Router = () => {
|
|
|
177
186
|
});
|
|
178
187
|
}
|
|
179
188
|
|
|
180
|
-
|
|
189
|
+
// Pagination
|
|
190
|
+
const pageSize = req.query.pageSize ? parseInt(req.query.pageSize as string, 10) : 100; // Default 100
|
|
191
|
+
let skip = 0;
|
|
192
|
+
if (req.query.pageToken) {
|
|
193
|
+
try {
|
|
194
|
+
const tokenJson = Buffer.from(req.query.pageToken as string, 'base64').toString('utf-8');
|
|
195
|
+
const tokenData = JSON.parse(tokenJson);
|
|
196
|
+
if (typeof tokenData.skip === 'number') {
|
|
197
|
+
skip = tokenData.skip;
|
|
198
|
+
}
|
|
199
|
+
} catch {
|
|
200
|
+
// Ignore invalid token, start from 0
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const totalFiles = files.length;
|
|
205
|
+
const resultFiles = files.slice(skip, skip + pageSize);
|
|
206
|
+
|
|
207
|
+
let nextPageToken: string | undefined;
|
|
208
|
+
if (skip + pageSize < totalFiles) {
|
|
209
|
+
const nextSkip = skip + pageSize;
|
|
210
|
+
nextPageToken = Buffer.from(JSON.stringify({ skip: nextSkip })).toString('base64');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const response: Record<string, unknown> = {
|
|
181
214
|
kind: "drive#fileList",
|
|
182
215
|
incompleteSearch: false,
|
|
183
|
-
files:
|
|
184
|
-
|
|
216
|
+
files: resultFiles,
|
|
217
|
+
nextPageToken
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const fields = req.query.fields as string;
|
|
221
|
+
if (fields) {
|
|
222
|
+
res.json(applyFields(response, fields));
|
|
223
|
+
} else {
|
|
224
|
+
res.json(response);
|
|
225
|
+
}
|
|
185
226
|
});
|
|
186
227
|
|
|
187
228
|
// Changes: Get Start Page Token
|
|
@@ -474,9 +515,11 @@ export const createV3Router = () => {
|
|
|
474
515
|
res.status(400).send("Invalid file ID");
|
|
475
516
|
return;
|
|
476
517
|
}
|
|
477
|
-
const updates = req.body;
|
|
518
|
+
const updates = req.body || {};
|
|
519
|
+
const hasBody = Object.keys(updates).length > 0;
|
|
520
|
+
const hasQueryParams = req.query.addParents || req.query.removeParents;
|
|
478
521
|
|
|
479
|
-
if (!
|
|
522
|
+
if (!hasBody && !hasQueryParams) {
|
|
480
523
|
res.status(400).json({ error: { code: 400, message: "Bad Request: No updates provided" } });
|
|
481
524
|
return;
|
|
482
525
|
}
|
|
@@ -488,6 +531,29 @@ export const createV3Router = () => {
|
|
|
488
531
|
return;
|
|
489
532
|
}
|
|
490
533
|
|
|
534
|
+
const addParents = req.query.addParents as string;
|
|
535
|
+
if (addParents) {
|
|
536
|
+
const parentsToAdd = addParents.split(',');
|
|
537
|
+
const currentParents = updatedFile.parents || [];
|
|
538
|
+
const newParents = [...new Set([...currentParents, ...parentsToAdd])]; // Union
|
|
539
|
+
// Update the file with new parents
|
|
540
|
+
const result = driveStore.updateFile(fileId, { parents: newParents });
|
|
541
|
+
if (result) {
|
|
542
|
+
Object.assign(updatedFile, result);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const removeParents = req.query.removeParents as string;
|
|
547
|
+
if (removeParents) {
|
|
548
|
+
const parentsToRemove = removeParents.split(',');
|
|
549
|
+
const currentParents = updatedFile.parents || [];
|
|
550
|
+
const newParents = currentParents.filter(p => !parentsToRemove.includes(p));
|
|
551
|
+
const result = driveStore.updateFile(fileId, { parents: newParents });
|
|
552
|
+
if (result) {
|
|
553
|
+
Object.assign(updatedFile, result);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
491
557
|
res.json(updatedFile);
|
|
492
558
|
});
|
|
493
559
|
|
package/src/store.ts
CHANGED
|
@@ -114,7 +114,7 @@ export class DriveStore {
|
|
|
114
114
|
...statsUpdates,
|
|
115
115
|
version: newVersion,
|
|
116
116
|
etag: String(newVersion),
|
|
117
|
-
modifiedTime: new Date().toISOString()
|
|
117
|
+
modifiedTime: updates.modifiedTime || new Date().toISOString()
|
|
118
118
|
};
|
|
119
119
|
this.files.set(id, updatedFile);
|
|
120
120
|
this.addChange(updatedFile);
|