google-drive-mock 1.0.6 → 1.0.7

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/dist/index.d.ts CHANGED
@@ -1,7 +1,4 @@
1
- interface AppConfig {
2
- serverLagBefore?: number;
3
- serverLagAfter?: number;
4
- }
1
+ import { AppConfig } from './types';
5
2
  declare const createApp: (config?: AppConfig) => import("express-serve-static-core").Express;
6
3
  declare const startServer: (port: number, host?: string, config?: AppConfig) => import("node:http").Server<typeof import("node:http").IncomingMessage, typeof import("node:http").ServerResponse>;
7
4
  export { createApp, startServer };
package/dist/index.js CHANGED
@@ -17,8 +17,13 @@ const express_1 = __importDefault(require("express"));
17
17
  const cors_1 = __importDefault(require("cors"));
18
18
  const store_1 = require("./store");
19
19
  const batch_1 = require("./batch");
20
- const mappers_1 = require("./mappers");
20
+ const v2_1 = require("./routes/v2");
21
+ const v3_1 = require("./routes/v3");
21
22
  const createApp = (config = {}) => {
23
+ // If apiEndpoint is not provided, default to localhost or empty (relative)
24
+ if (!config.apiEndpoint) {
25
+ config.apiEndpoint = "";
26
+ }
22
27
  const app = (0, express_1.default)();
23
28
  app.use((0, cors_1.default)({
24
29
  exposedHeaders: ['ETag']
@@ -69,467 +74,9 @@ const createApp = (config = {}) => {
69
74
  }
70
75
  next();
71
76
  });
72
- // Middleware to simulate some Google API behaviors (optional, can be expanded)
73
- // About
74
- app.get('/drive/v3/about', (req, res) => {
75
- const about = store_1.driveStore.getAbout();
76
- res.json(Object.assign({ kind: "drive#about" }, about));
77
- });
78
- // Files: List
79
- app.get('/drive/v3/files', (req, res) => {
80
- let files = store_1.driveStore.listFiles();
81
- const q = req.query.q;
82
- const orderBy = req.query.orderBy;
83
- if (q) {
84
- // Enhanced query parser for Mock
85
- // Supports:
86
- // - name = '...'
87
- // - mimeType = '...'
88
- // - trashed = true/false
89
- // - 'ID' in parents
90
- // - name contains '...'
91
- const parts = q.split(' and ').map(p => p.trim());
92
- files = files.filter(file => {
93
- return parts.every(part => {
94
- var _a, _b, _c, _d, _e, _f, _g, _h;
95
- // name = '...'
96
- if (part.startsWith("name = '")) {
97
- const name = (_a = part.match(/name = '(.*)'/)) === null || _a === void 0 ? void 0 : _a[1];
98
- return file.name === name;
99
- }
100
- // name contains '...'
101
- if (part.startsWith("name contains '")) {
102
- const token = (_b = part.match(/name contains '(.*)'/)) === null || _b === void 0 ? void 0 : _b[1];
103
- return token && file.name.includes(token);
104
- }
105
- // 'ID' in parents
106
- if (part.includes(" in parents")) {
107
- const parentId = (_c = part.match(/'(.*)' in parents/)) === null || _c === void 0 ? void 0 : _c[1];
108
- return parentId && ((_d = file.parents) === null || _d === void 0 ? void 0 : _d.includes(parentId));
109
- }
110
- // trashed = ...
111
- if (part === "trashed = false") {
112
- return file.trashed !== true;
113
- }
114
- if (part === "trashed = true") {
115
- return file.trashed === true;
116
- }
117
- // mimeType = '...'
118
- if (part.startsWith("mimeType = '")) {
119
- const mime = (_e = part.match(/mimeType = '(.*)'/)) === null || _e === void 0 ? void 0 : _e[1];
120
- return file.mimeType === mime;
121
- }
122
- // mimeType != '...'
123
- if (part.startsWith("mimeType != '")) {
124
- const mime = (_f = part.match(/mimeType != '(.*)'/)) === null || _f === void 0 ? void 0 : _f[1];
125
- return file.mimeType !== mime;
126
- }
127
- // modifiedTime > '...'
128
- if (part.startsWith("modifiedTime > '")) {
129
- const timeStr = (_g = part.match(/modifiedTime > '(.*)'/)) === null || _g === void 0 ? void 0 : _g[1];
130
- return timeStr && new Date(file.modifiedTime) > new Date(timeStr);
131
- }
132
- // modifiedTime < '...'
133
- if (part.startsWith("modifiedTime < '")) {
134
- const timeStr = (_h = part.match(/modifiedTime < '(.*)'/)) === null || _h === void 0 ? void 0 : _h[1];
135
- return timeStr && new Date(file.modifiedTime) < new Date(timeStr);
136
- }
137
- // Ignore unknown filters for now
138
- return true;
139
- });
140
- });
141
- }
142
- // Sorting (orderBy)
143
- if (orderBy) {
144
- // Basic support for single keys: 'folder,name', 'modifiedTime desc', etc.
145
- // Splitting by comma
146
- const sortKeys = orderBy.split(',').map(k => k.trim());
147
- files.sort((a, b) => {
148
- for (const keyDef of sortKeys) {
149
- const [key, direction] = keyDef.split(' ');
150
- const dir = direction || 'asc';
151
- // Handle special virtual key 'folder'
152
- if (key === 'folder') {
153
- const aIsFolder = a.mimeType === 'application/vnd.google-apps.folder';
154
- const bIsFolder = b.mimeType === 'application/vnd.google-apps.folder';
155
- if (aIsFolder !== bIsFolder) {
156
- // Folders first in 'folder' sort usually?
157
- // Google docs say: "folder sets folders to appear before..."
158
- const valA = aIsFolder ? 0 : 1;
159
- const valB = bIsFolder ? 0 : 1;
160
- if (valA !== valB)
161
- return dir === 'desc' ? valB - valA : valA - valB;
162
- }
163
- continue;
164
- }
165
- const valA = a[key];
166
- const valB = b[key];
167
- if (valA === undefined || valB === undefined)
168
- return 0;
169
- if (valA < valB)
170
- return dir === 'desc' ? 1 : -1;
171
- if (valA > valB)
172
- return dir === 'desc' ? -1 : 1;
173
- }
174
- return 0;
175
- });
176
- }
177
- res.json({
178
- kind: "drive#fileList",
179
- incompleteSearch: false,
180
- files: files
181
- });
182
- });
183
- // Changes: Get Start Page Token
184
- app.get('/drive/v3/changes/startPageToken', (req, res) => {
185
- const token = store_1.driveStore.getStartPageToken();
186
- res.json({
187
- kind: "drive#startPageToken",
188
- startPageToken: token
189
- });
190
- });
191
- // Changes: List
192
- app.get('/drive/v3/changes', (req, res) => {
193
- const pageToken = req.query.pageToken;
194
- if (!pageToken) {
195
- res.status(400).json({ error: { code: 400, message: "Bad Request: pageToken is required" } });
196
- return;
197
- }
198
- const result = store_1.driveStore.getChanges(pageToken);
199
- res.json({
200
- kind: "drive#changeList",
201
- newStartPageToken: result.newStartPageToken,
202
- nextPageToken: result.nextPageToken,
203
- changes: result.changes
204
- });
205
- });
206
- // Upload Files Route
207
- app.post('/upload/drive/v3/files', (req, res) => {
208
- const uploadType = req.query.uploadType;
209
- if (uploadType !== 'multipart') {
210
- res.status(400).json({ error: { code: 400, message: "Only uploadType=multipart is supported in this mock route" } });
211
- return;
212
- }
213
- const contentTypeHeader = req.headers['content-type'];
214
- const contentType = Array.isArray(contentTypeHeader) ? contentTypeHeader[0] : contentTypeHeader;
215
- if (!contentType || !contentType.includes('multipart/related')) {
216
- res.status(400).json({ error: { code: 400, message: "Content-Type must be multipart/related" } });
217
- return;
218
- }
219
- const boundaryMatch = contentType.match(/boundary=(.+)/);
220
- if (!boundaryMatch) {
221
- res.status(400).json({ error: { code: 400, message: "Multipart boundary missing" } });
222
- return;
223
- }
224
- let boundary = boundaryMatch[1];
225
- if (boundary.startsWith('"') && boundary.endsWith('"')) {
226
- boundary = boundary.substring(1, boundary.length - 1);
227
- }
228
- const rawBody = req.body;
229
- if (typeof rawBody !== 'string') {
230
- res.status(400).json({ error: { code: 400, message: "Body parsing failed" } });
231
- return;
232
- }
233
- // Simple Multipart Parsing
234
- const parts = rawBody.split(`--${boundary}`);
235
- // Part 0 is usually empty (preamble)
236
- // Part 1 is Metadata
237
- // Part 2 is Content
238
- // Last part is --
239
- const validParts = parts.filter(p => p.trim() !== '' && p.trim() !== '--');
240
- if (validParts.length < 2) {
241
- res.status(400).json({ error: { code: 400, message: "Invalid multipart body: expected at least metadata and content" } });
242
- return;
243
- }
244
- const parsePart = (rawPart) => {
245
- const splitIndex = rawPart.indexOf('\r\n\r\n');
246
- if (splitIndex === -1)
247
- return null;
248
- const headers = rawPart.substring(0, splitIndex).trim();
249
- const body = rawPart.substring(splitIndex + 4); // No trim at end to preserve content whitespace?
250
- // Actually Multipart usually has \r\n at end before boundary, so we might want to trim that.
251
- // But relying on split --boundary usually leaves the preceding \r\n attached to the body part?
252
- // split uses the separator.
253
- // "Part1\r\n--boundary\r\nPart2"
254
- // Split by --boundary: ["Part1\r\n", "\r\nPart2"]
255
- // So Part1 has a trailing \r\n.
256
- return {
257
- headers,
258
- body: body.replace(/\r\n$/, '') // Remove trailing CRLF
259
- };
260
- };
261
- const metadataPart = parsePart(validParts[0]);
262
- const contentPart = parsePart(validParts[1]);
263
- if (!metadataPart || !contentPart) {
264
- res.status(400).json({ error: { code: 400, message: "Failed to parse parts" } });
265
- return;
266
- }
267
- let metadata;
268
- try {
269
- metadata = JSON.parse(metadataPart.body);
270
- }
271
- catch (_a) {
272
- res.status(400).json({ error: { code: 400, message: "Invalid JSON in metadata part" } });
273
- return;
274
- }
275
- let content;
276
- // Try to parse content as JSON if applicable, else keep as string?
277
- // In the user request, it is JSON content.
278
- // And store expects 'content' property to be anything.
279
- try {
280
- content = JSON.parse(contentPart.body);
281
- }
282
- catch (_b) {
283
- content = contentPart.body;
284
- }
285
- // Create File
286
- // Ensure name uniqueness check if needed (reusing logic from normal create)
287
- const existing = store_1.driveStore.listFiles().find(f => {
288
- if (f.name !== metadata.name)
289
- return false;
290
- // Filter trashed?
291
- if (f.trashed)
292
- return false;
293
- const newParents = metadata.parents || [];
294
- const existingParents = f.parents || [];
295
- // If both new and existing have NO parents, they are both in root -> Conflict
296
- if (newParents.length === 0 && existingParents.length === 0)
297
- return true;
298
- // Check intersection of parents
299
- return newParents.some((p) => existingParents.includes(p));
300
- });
301
- if (existing) {
302
- res.status(409).json({ error: { code: 409, message: "Conflict: File with same name already exists" } });
303
- return;
304
- }
305
- const newFile = store_1.driveStore.createFile(Object.assign(Object.assign({}, metadata), { content: content }));
306
- res.status(200).json(newFile);
307
- });
308
- // Files: Create (Standard)
309
- app.post('/drive/v3/files', (req, res) => {
310
- const body = req.body || {};
311
- // Real API allows missing name (defaults to "Untitled"?) or just works.
312
- // Parity: Allow missing name.
313
- const name = body.name || "Untitled";
314
- // Enforce Unique Name Constraint (Mock Behavior customization)
315
- // Real API allows duplicates. Removing constraint for parity.
316
- /*
317
- const existing = driveStore.listFiles().find(f => {
318
- if (f.name !== body.name) return false;
319
- // ...
320
- return newParents.some((p: string) => existingParents.includes(p));
321
- });
322
-
323
- if (existing) {
324
- res.status(409).json({ error: { code: 409, message: "Conflict: File with same name already exists" } });
325
- return;
326
- }
327
- */
328
- const newFile = store_1.driveStore.createFile(Object.assign(Object.assign({}, body), { name: name, mimeType: body.mimeType || "application/octet-stream", parents: body.parents || [] }));
329
- res.status(200).json(newFile);
330
- });
331
- // Files: Get
332
- app.get('/drive/v3/files/:fileId', (req, res) => {
333
- const fileId = req.params.fileId;
334
- if (typeof fileId !== 'string') {
335
- res.status(400).send("Invalid file ID");
336
- return;
337
- }
338
- const file = store_1.driveStore.getFile(fileId);
339
- if (!file) {
340
- res.status(404).json({ error: { code: 404, message: "File not found" } });
341
- return;
342
- }
343
- // Parity: Real V3 API returns 400 if 'etag' is requested in fields
344
- const fields = req.query.fields;
345
- if (fields && (fields.includes('etag') || fields.includes('kind,etag'))) {
346
- res.status(400).json({ error: { code: 400, message: "Invalid field selection: etag" } });
347
- return;
348
- }
349
- // Mock does not return ETag header because Real API (v3) does not return it by default/in this context.
350
- // res.setHeader('ETag', etag);
351
- // Real API also ignores If-None-Match if ETag is not supported?
352
- // match behavior: do nothing.
353
- /*
354
- if (req.headers['if-none-match'] === etag) {
355
- res.status(304).end();
356
- return;
357
- }
358
- */
359
- res.json(file);
360
- });
361
- // Files: Update
362
- app.patch('/drive/v3/files/:fileId', (req, res) => {
363
- const fileId = req.params.fileId;
364
- if (typeof fileId !== 'string') {
365
- res.status(400).send("Invalid file ID");
366
- return;
367
- }
368
- const updates = req.body;
369
- if (!updates) {
370
- res.status(400).json({ error: { code: 400, message: "Bad Request: No updates provided" } });
371
- return;
372
- }
373
- // Check for Precondition (If-Match)
374
- // Real Google Drive API V3 observed behavior: Ignores If-Match on PATCH (Last Write Wins).
375
- // Mock matches this Parity.
376
- /*
377
- const existingFile = driveStore.getFile(fileId);
378
- if (existingFile) {
379
- const ifMatch = req.headers['if-match'];
380
- if (ifMatch && ifMatch !== '*' && ifMatch !== existingFile.etag) {
381
- // Also support quoted etag if user sends it
382
- if (ifMatch !== `"${existingFile.etag}"`) {
383
- res.status(412).json({ error: { code: 412, message: "Precondition Failed" } });
384
- return;
385
- }
386
- }
387
- }
388
- */
389
- const updatedFile = store_1.driveStore.updateFile(fileId, updates);
390
- if (!updatedFile) {
391
- res.status(404).json({ error: { code: 404, message: "File not found" } });
392
- return;
393
- }
394
- res.json(updatedFile);
395
- });
396
- // Files: Delete
397
- app.delete('/drive/v3/files/:fileId', (req, res) => {
398
- const fileId = req.params.fileId;
399
- if (typeof fileId !== 'string') {
400
- res.status(400).send("Invalid file ID");
401
- return;
402
- }
403
- // Check for Precondition (If-Match)
404
- // Real API behavior: Ignores If-Match (returns 204 even on mismatch)
405
- /*
406
- const existingFile = driveStore.getFile(fileId);
407
- if (existingFile) {
408
- const ifMatch = req.headers['if-match'];
409
- if (ifMatch && ifMatch !== '*' && ifMatch !== existingFile.etag) {
410
- // Strict logic removed for Parity
411
- }
412
- }
413
- */
414
- const deleted = store_1.driveStore.deleteFile(fileId);
415
- if (!deleted) {
416
- // According to Google API, delete might return 404 if not found, or 204 if successful (or 200).
417
- // Docs says "If successful, this method returns an empty response body." usually 204.
418
- // But if not found:
419
- res.status(404).json({ error: { code: 404, message: "File not found" } });
420
- return;
421
- }
422
- res.status(204).send();
423
- });
424
- // ==========================================
425
- // Google Drive API V2 Routes
426
- // ==========================================
427
- // V2 Files: Create
428
- app.post('/drive/v2/files', (req, res) => {
429
- const v2Body = req.body || {};
430
- const fileData = (0, mappers_1.fromV2Update)(v2Body);
431
- // V2 typical defaults
432
- const name = fileData.name || v2Body.title || "Untitled"; // Fallback if mapper missed it or explicit
433
- const newFile = store_1.driveStore.createFile(Object.assign(Object.assign({}, fileData), { name: name, mimeType: fileData.mimeType || "application/octet-stream", parents: fileData.parents || [] }));
434
- res.status(200).json((0, mappers_1.toV2File)(newFile));
435
- });
436
- // V2 Files: Get
437
- app.get('/drive/v2/files/:fileId', (req, res) => {
438
- const fileId = req.params.fileId;
439
- if (typeof fileId !== 'string') {
440
- res.status(400).send("Invalid file ID");
441
- return;
442
- }
443
- const file = store_1.driveStore.getFile(fileId);
444
- if (!file) {
445
- res.status(404).json({ error: { code: 404, message: "File not found" } });
446
- return;
447
- }
448
- // V2 ETag handling - usually sends ETag header
449
- if (file.etag) {
450
- res.setHeader('ETag', file.etag);
451
- }
452
- res.json((0, mappers_1.toV2File)(file));
453
- });
454
- // V2 Files: Update (PUT)
455
- app.put('/drive/v2/files/:fileId', (req, res) => {
456
- const fileId = req.params.fileId;
457
- if (typeof fileId !== 'string') {
458
- res.status(400).send("Invalid file ID");
459
- return;
460
- }
461
- const v2Body = req.body || {};
462
- const updates = (0, mappers_1.fromV2Update)(v2Body);
463
- const existingFile = store_1.driveStore.getFile(fileId);
464
- if (!existingFile) {
465
- res.status(404).json({ error: { code: 404, message: "File not found" } });
466
- return;
467
- }
468
- // Check for Precondition (If-Match)
469
- const ifMatchHeader = req.headers['if-match'];
470
- const ifMatch = Array.isArray(ifMatchHeader) ? ifMatchHeader[0] : ifMatchHeader;
471
- if (ifMatch && ifMatch !== '*' && ifMatch !== existingFile.etag) {
472
- // Also support quoted etag if user sends it
473
- // Internal etag might be "version", validation needs exact match
474
- if (ifMatch !== existingFile.etag && ifMatch !== `"${existingFile.etag}"`) {
475
- res.status(412).json({ error: { code: 412, message: "Precondition Failed" } });
476
- return;
477
- }
478
- }
479
- const updatedFile = store_1.driveStore.updateFile(fileId, updates);
480
- res.json((0, mappers_1.toV2File)(updatedFile));
481
- });
482
- // V2 Files: Patch (PATCH)
483
- app.patch('/drive/v2/files/:fileId', (req, res) => {
484
- const fileId = req.params.fileId;
485
- if (typeof fileId !== 'string') {
486
- res.status(400).send("Invalid file ID");
487
- return;
488
- }
489
- const v2Body = req.body || {};
490
- const updates = (0, mappers_1.fromV2Update)(v2Body);
491
- const existingFile = store_1.driveStore.getFile(fileId);
492
- if (!existingFile) {
493
- res.status(404).json({ error: { code: 404, message: "File not found" } });
494
- return;
495
- }
496
- // Check for Precondition (If-Match)
497
- const ifMatchHeader = req.headers['if-match'];
498
- const ifMatch = Array.isArray(ifMatchHeader) ? ifMatchHeader[0] : ifMatchHeader;
499
- if (ifMatch && ifMatch !== '*' && ifMatch !== existingFile.etag) {
500
- if (ifMatch !== existingFile.etag && ifMatch !== `"${existingFile.etag}"`) {
501
- res.status(412).json({ error: { code: 412, message: "Precondition Failed" } });
502
- return;
503
- }
504
- }
505
- const updatedFile = store_1.driveStore.updateFile(fileId, updates);
506
- res.json((0, mappers_1.toV2File)(updatedFile));
507
- });
508
- // V2 Files: Delete
509
- app.delete('/drive/v2/files/:fileId', (req, res) => {
510
- const fileId = req.params.fileId;
511
- if (typeof fileId !== 'string') {
512
- res.status(400).send("Invalid file ID");
513
- return;
514
- }
515
- const existingFile = store_1.driveStore.getFile(fileId);
516
- // V2 specific: often returns 404 for not found, same as V3 check
517
- if (!existingFile) {
518
- res.status(404).json({ error: { code: 404, message: "File not found" } });
519
- return;
520
- }
521
- // Check for Precondition (If-Match) - V2 respects this more often
522
- const ifMatchHeader = req.headers['if-match'];
523
- const ifMatch = Array.isArray(ifMatchHeader) ? ifMatchHeader[0] : ifMatchHeader;
524
- if (ifMatch && ifMatch !== '*' && ifMatch !== existingFile.etag) {
525
- if (ifMatch !== existingFile.etag && ifMatch !== `"${existingFile.etag}"`) {
526
- res.status(412).json({ error: { code: 412, message: "Precondition Failed" } });
527
- return;
528
- }
529
- }
530
- store_1.driveStore.deleteFile(fileId);
531
- res.status(204).send();
532
- });
77
+ // Mount Routers
78
+ app.use((0, v3_1.createV3Router)());
79
+ app.use((0, v2_1.createV2Router)(config));
533
80
  return app;
534
81
  };
535
82
  exports.createApp = createApp;
@@ -0,0 +1,2 @@
1
+ import { AppConfig } from '../types';
2
+ export declare const createV2Router: (config: AppConfig) => import("express-serve-static-core").Router;