google-drive-mock 1.0.5 → 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,9 +17,17 @@ 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 v2_1 = require("./routes/v2");
21
+ const v3_1 = require("./routes/v3");
20
22
  const createApp = (config = {}) => {
23
+ // If apiEndpoint is not provided, default to localhost or empty (relative)
24
+ if (!config.apiEndpoint) {
25
+ config.apiEndpoint = "";
26
+ }
21
27
  const app = (0, express_1.default)();
22
- app.use((0, cors_1.default)());
28
+ app.use((0, cors_1.default)({
29
+ exposedHeaders: ['ETag']
30
+ }));
23
31
  app.set('etag', false); // Disable default ETag generation to match Real API behavior
24
32
  app.use((req, res, next) => __awaiter(void 0, void 0, void 0, function* () {
25
33
  if (config.serverLagBefore && config.serverLagBefore > 0) {
@@ -53,7 +61,8 @@ const createApp = (config = {}) => {
53
61
  // Auth Middleware
54
62
  const validTokens = ['valid-token', 'another-valid-token'];
55
63
  app.use((req, res, next) => {
56
- const authHeader = req.headers.authorization;
64
+ const authHeaderVal = req.headers.authorization;
65
+ const authHeader = Array.isArray(authHeaderVal) ? authHeaderVal[0] : authHeaderVal;
57
66
  if (!authHeader) {
58
67
  res.status(401).json({ error: { code: 401, message: "Unauthorized: No token provided" } });
59
68
  return;
@@ -65,351 +74,9 @@ const createApp = (config = {}) => {
65
74
  }
66
75
  next();
67
76
  });
68
- // Middleware to simulate some Google API behaviors (optional, can be expanded)
69
- // About
70
- app.get('/drive/v3/about', (req, res) => {
71
- const about = store_1.driveStore.getAbout();
72
- res.json(Object.assign({ kind: "drive#about" }, about));
73
- });
74
- // Files: List
75
- app.get('/drive/v3/files', (req, res) => {
76
- let files = store_1.driveStore.listFiles();
77
- const q = req.query.q;
78
- const orderBy = req.query.orderBy;
79
- if (q) {
80
- // Enhanced query parser for Mock
81
- // Supports:
82
- // - name = '...'
83
- // - mimeType = '...'
84
- // - trashed = true/false
85
- // - 'ID' in parents
86
- // - name contains '...'
87
- const parts = q.split(' and ').map(p => p.trim());
88
- files = files.filter(file => {
89
- return parts.every(part => {
90
- var _a, _b, _c, _d, _e, _f, _g, _h;
91
- // name = '...'
92
- if (part.startsWith("name = '")) {
93
- const name = (_a = part.match(/name = '(.*)'/)) === null || _a === void 0 ? void 0 : _a[1];
94
- return file.name === name;
95
- }
96
- // name contains '...'
97
- if (part.startsWith("name contains '")) {
98
- const token = (_b = part.match(/name contains '(.*)'/)) === null || _b === void 0 ? void 0 : _b[1];
99
- return token && file.name.includes(token);
100
- }
101
- // 'ID' in parents
102
- if (part.includes(" in parents")) {
103
- const parentId = (_c = part.match(/'(.*)' in parents/)) === null || _c === void 0 ? void 0 : _c[1];
104
- return parentId && ((_d = file.parents) === null || _d === void 0 ? void 0 : _d.includes(parentId));
105
- }
106
- // trashed = ...
107
- if (part === "trashed = false") {
108
- return file.trashed !== true;
109
- }
110
- if (part === "trashed = true") {
111
- return file.trashed === true;
112
- }
113
- // mimeType = '...'
114
- if (part.startsWith("mimeType = '")) {
115
- const mime = (_e = part.match(/mimeType = '(.*)'/)) === null || _e === void 0 ? void 0 : _e[1];
116
- return file.mimeType === mime;
117
- }
118
- // mimeType != '...'
119
- if (part.startsWith("mimeType != '")) {
120
- const mime = (_f = part.match(/mimeType != '(.*)'/)) === null || _f === void 0 ? void 0 : _f[1];
121
- return file.mimeType !== mime;
122
- }
123
- // modifiedTime > '...'
124
- if (part.startsWith("modifiedTime > '")) {
125
- const timeStr = (_g = part.match(/modifiedTime > '(.*)'/)) === null || _g === void 0 ? void 0 : _g[1];
126
- return timeStr && new Date(file.modifiedTime) > new Date(timeStr);
127
- }
128
- // modifiedTime < '...'
129
- if (part.startsWith("modifiedTime < '")) {
130
- const timeStr = (_h = part.match(/modifiedTime < '(.*)'/)) === null || _h === void 0 ? void 0 : _h[1];
131
- return timeStr && new Date(file.modifiedTime) < new Date(timeStr);
132
- }
133
- // Ignore unknown filters for now
134
- return true;
135
- });
136
- });
137
- }
138
- // Sorting (orderBy)
139
- if (orderBy) {
140
- // Basic support for single keys: 'folder,name', 'modifiedTime desc', etc.
141
- // Splitting by comma
142
- const sortKeys = orderBy.split(',').map(k => k.trim());
143
- files.sort((a, b) => {
144
- for (const keyDef of sortKeys) {
145
- const [key, direction] = keyDef.split(' ');
146
- const dir = direction || 'asc';
147
- // Handle special virtual key 'folder'
148
- if (key === 'folder') {
149
- const aIsFolder = a.mimeType === 'application/vnd.google-apps.folder';
150
- const bIsFolder = b.mimeType === 'application/vnd.google-apps.folder';
151
- if (aIsFolder !== bIsFolder) {
152
- // Folders first in 'folder' sort usually?
153
- // Google docs say: "folder sets folders to appear before..."
154
- const valA = aIsFolder ? 0 : 1;
155
- const valB = bIsFolder ? 0 : 1;
156
- if (valA !== valB)
157
- return dir === 'desc' ? valB - valA : valA - valB;
158
- }
159
- continue;
160
- }
161
- const valA = a[key];
162
- const valB = b[key];
163
- if (valA === undefined || valB === undefined)
164
- return 0;
165
- if (valA < valB)
166
- return dir === 'desc' ? 1 : -1;
167
- if (valA > valB)
168
- return dir === 'desc' ? -1 : 1;
169
- }
170
- return 0;
171
- });
172
- }
173
- res.json({
174
- kind: "drive#fileList",
175
- incompleteSearch: false,
176
- files: files
177
- });
178
- });
179
- // Changes: Get Start Page Token
180
- app.get('/drive/v3/changes/startPageToken', (req, res) => {
181
- const token = store_1.driveStore.getStartPageToken();
182
- res.json({
183
- kind: "drive#startPageToken",
184
- startPageToken: token
185
- });
186
- });
187
- // Changes: List
188
- app.get('/drive/v3/changes', (req, res) => {
189
- const pageToken = req.query.pageToken;
190
- if (!pageToken) {
191
- res.status(400).json({ error: { code: 400, message: "Bad Request: pageToken is required" } });
192
- return;
193
- }
194
- const result = store_1.driveStore.getChanges(pageToken);
195
- res.json({
196
- kind: "drive#changeList",
197
- newStartPageToken: result.newStartPageToken,
198
- nextPageToken: result.nextPageToken,
199
- changes: result.changes
200
- });
201
- });
202
- // Upload Files Route
203
- app.post('/upload/drive/v3/files', (req, res) => {
204
- const uploadType = req.query.uploadType;
205
- if (uploadType !== 'multipart') {
206
- res.status(400).json({ error: { code: 400, message: "Only uploadType=multipart is supported in this mock route" } });
207
- return;
208
- }
209
- const contentType = req.headers['content-type'];
210
- if (!contentType || !contentType.includes('multipart/related')) {
211
- res.status(400).json({ error: { code: 400, message: "Content-Type must be multipart/related" } });
212
- return;
213
- }
214
- const boundaryMatch = contentType.match(/boundary=(.+)/);
215
- if (!boundaryMatch) {
216
- res.status(400).json({ error: { code: 400, message: "Multipart boundary missing" } });
217
- return;
218
- }
219
- let boundary = boundaryMatch[1];
220
- if (boundary.startsWith('"') && boundary.endsWith('"')) {
221
- boundary = boundary.substring(1, boundary.length - 1);
222
- }
223
- const rawBody = req.body;
224
- if (typeof rawBody !== 'string') {
225
- res.status(400).json({ error: { code: 400, message: "Body parsing failed" } });
226
- return;
227
- }
228
- // Simple Multipart Parsing
229
- const parts = rawBody.split(`--${boundary}`);
230
- // Part 0 is usually empty (preamble)
231
- // Part 1 is Metadata
232
- // Part 2 is Content
233
- // Last part is --
234
- const validParts = parts.filter(p => p.trim() !== '' && p.trim() !== '--');
235
- if (validParts.length < 2) {
236
- res.status(400).json({ error: { code: 400, message: "Invalid multipart body: expected at least metadata and content" } });
237
- return;
238
- }
239
- const parsePart = (rawPart) => {
240
- const splitIndex = rawPart.indexOf('\r\n\r\n');
241
- if (splitIndex === -1)
242
- return null;
243
- const headers = rawPart.substring(0, splitIndex).trim();
244
- const body = rawPart.substring(splitIndex + 4); // No trim at end to preserve content whitespace?
245
- // Actually Multipart usually has \r\n at end before boundary, so we might want to trim that.
246
- // But relying on split --boundary usually leaves the preceding \r\n attached to the body part?
247
- // split uses the separator.
248
- // "Part1\r\n--boundary\r\nPart2"
249
- // Split by --boundary: ["Part1\r\n", "\r\nPart2"]
250
- // So Part1 has a trailing \r\n.
251
- return {
252
- headers,
253
- body: body.replace(/\r\n$/, '') // Remove trailing CRLF
254
- };
255
- };
256
- const metadataPart = parsePart(validParts[0]);
257
- const contentPart = parsePart(validParts[1]);
258
- if (!metadataPart || !contentPart) {
259
- res.status(400).json({ error: { code: 400, message: "Failed to parse parts" } });
260
- return;
261
- }
262
- let metadata;
263
- try {
264
- metadata = JSON.parse(metadataPart.body);
265
- }
266
- catch (_a) {
267
- res.status(400).json({ error: { code: 400, message: "Invalid JSON in metadata part" } });
268
- return;
269
- }
270
- let content;
271
- // Try to parse content as JSON if applicable, else keep as string?
272
- // In the user request, it is JSON content.
273
- // And store expects 'content' property to be anything.
274
- try {
275
- content = JSON.parse(contentPart.body);
276
- }
277
- catch (_b) {
278
- content = contentPart.body;
279
- }
280
- // Create File
281
- // Ensure name uniqueness check if needed (reusing logic from normal create)
282
- const existing = store_1.driveStore.listFiles().find(f => {
283
- if (f.name !== metadata.name)
284
- return false;
285
- // Filter trashed?
286
- if (f.trashed)
287
- return false;
288
- const newParents = metadata.parents || [];
289
- const existingParents = f.parents || [];
290
- // If both new and existing have NO parents, they are both in root -> Conflict
291
- if (newParents.length === 0 && existingParents.length === 0)
292
- return true;
293
- // Check intersection of parents
294
- return newParents.some((p) => existingParents.includes(p));
295
- });
296
- if (existing) {
297
- res.status(409).json({ error: { code: 409, message: "Conflict: File with same name already exists" } });
298
- return;
299
- }
300
- const newFile = store_1.driveStore.createFile(Object.assign(Object.assign({}, metadata), { content: content }));
301
- res.status(200).json(newFile);
302
- });
303
- // Files: Create (Standard)
304
- app.post('/drive/v3/files', (req, res) => {
305
- const body = req.body || {};
306
- // Real API allows missing name (defaults to "Untitled"?) or just works.
307
- // Parity: Allow missing name.
308
- const name = body.name || "Untitled";
309
- // Enforce Unique Name Constraint (Mock Behavior customization)
310
- // Real API allows duplicates. Removing constraint for parity.
311
- /*
312
- const existing = driveStore.listFiles().find(f => {
313
- if (f.name !== body.name) return false;
314
- // ...
315
- return newParents.some((p: string) => existingParents.includes(p));
316
- });
317
-
318
- if (existing) {
319
- res.status(409).json({ error: { code: 409, message: "Conflict: File with same name already exists" } });
320
- return;
321
- }
322
- */
323
- const newFile = store_1.driveStore.createFile(Object.assign(Object.assign({}, body), { name: name, mimeType: body.mimeType || "application/octet-stream", parents: body.parents || [] }));
324
- res.status(200).json(newFile);
325
- });
326
- // Files: Get
327
- app.get('/drive/v3/files/:fileId', (req, res) => {
328
- const fileId = req.params.fileId;
329
- if (typeof fileId !== 'string') {
330
- res.status(400).send("Invalid file ID");
331
- return;
332
- }
333
- const file = store_1.driveStore.getFile(fileId);
334
- if (!file) {
335
- res.status(404).json({ error: { code: 404, message: "File not found" } });
336
- return;
337
- }
338
- // Mock does not return ETag header because Real API (v3) does not return it by default/in this context.
339
- // res.setHeader('ETag', etag);
340
- // Real API also ignores If-None-Match if ETag is not supported?
341
- // match behavior: do nothing.
342
- /*
343
- if (req.headers['if-none-match'] === etag) {
344
- res.status(304).end();
345
- return;
346
- }
347
- */
348
- res.json(file);
349
- });
350
- // Files: Update
351
- app.patch('/drive/v3/files/:fileId', (req, res) => {
352
- const fileId = req.params.fileId;
353
- if (typeof fileId !== 'string') {
354
- res.status(400).send("Invalid file ID");
355
- return;
356
- }
357
- const updates = req.body;
358
- if (!updates) {
359
- res.status(400).json({ error: { code: 400, message: "Bad Request: No updates provided" } });
360
- return;
361
- }
362
- // Check for Precondition (If-Match)
363
- // Real Google Drive API V3 observed behavior: Ignores If-Match on PATCH (Last Write Wins).
364
- // Mock matches this Parity.
365
- /*
366
- const existingFile = driveStore.getFile(fileId);
367
- if (existingFile) {
368
- const ifMatch = req.headers['if-match'];
369
- if (ifMatch && ifMatch !== '*' && ifMatch !== existingFile.etag) {
370
- // Also support quoted etag if user sends it
371
- if (ifMatch !== `"${existingFile.etag}"`) {
372
- res.status(412).json({ error: { code: 412, message: "Precondition Failed" } });
373
- return;
374
- }
375
- }
376
- }
377
- */
378
- const updatedFile = store_1.driveStore.updateFile(fileId, updates);
379
- if (!updatedFile) {
380
- res.status(404).json({ error: { code: 404, message: "File not found" } });
381
- return;
382
- }
383
- res.json(updatedFile);
384
- });
385
- // Files: Delete
386
- app.delete('/drive/v3/files/:fileId', (req, res) => {
387
- const fileId = req.params.fileId;
388
- if (typeof fileId !== 'string') {
389
- res.status(400).send("Invalid file ID");
390
- return;
391
- }
392
- // Check for Precondition (If-Match)
393
- // Real API behavior: Ignores If-Match (returns 204 even on mismatch)
394
- /*
395
- const existingFile = driveStore.getFile(fileId);
396
- if (existingFile) {
397
- const ifMatch = req.headers['if-match'];
398
- if (ifMatch && ifMatch !== '*' && ifMatch !== existingFile.etag) {
399
- // Strict logic removed for Parity
400
- }
401
- }
402
- */
403
- const deleted = store_1.driveStore.deleteFile(fileId);
404
- if (!deleted) {
405
- // According to Google API, delete might return 404 if not found, or 204 if successful (or 200).
406
- // Docs says "If successful, this method returns an empty response body." usually 204.
407
- // But if not found:
408
- res.status(404).json({ error: { code: 404, message: "File not found" } });
409
- return;
410
- }
411
- res.status(204).send();
412
- });
77
+ // Mount Routers
78
+ app.use((0, v3_1.createV3Router)());
79
+ app.use((0, v2_1.createV2Router)(config));
413
80
  return app;
414
81
  };
415
82
  exports.createApp = createApp;
@@ -0,0 +1,9 @@
1
+ import { DriveFile } from './store';
2
+ /**
3
+ * Maps an internal DriveFile (V3 format) to a V2 API File resource.
4
+ */
5
+ export declare function toV2File(file: DriveFile): Record<string, unknown>;
6
+ /**
7
+ * Maps a V2 API File Update/Insert body to a partial Internal DriveFile (V3 format).
8
+ */
9
+ export declare function fromV2Update(body: Record<string, unknown>): Partial<DriveFile>;
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toV2File = toV2File;
4
+ exports.fromV2Update = fromV2Update;
5
+ /**
6
+ * Maps an internal DriveFile (V3 format) to a V2 API File resource.
7
+ */
8
+ function toV2File(file) {
9
+ return {
10
+ kind: 'drive#file',
11
+ id: file.id,
12
+ etag: file.etag || `"${file.version}"`, // V2 uses etags frequently
13
+ selfLink: `http://localhost/drive/v2/files/${file.id}`, // Mock link
14
+ title: file.name,
15
+ mimeType: file.mimeType,
16
+ labels: {
17
+ starred: file.starred || false,
18
+ hidden: false,
19
+ trashed: file.trashed || false,
20
+ restricted: false,
21
+ viewed: true
22
+ },
23
+ createdDate: file.createdTime,
24
+ modifiedDate: file.modifiedTime,
25
+ parents: (file.parents || []).map(parentId => ({
26
+ kind: 'drive#parentReference',
27
+ id: parentId,
28
+ selfLink: `http://localhost/drive/v2/files/${parentId}`,
29
+ parentLink: `http://localhost/drive/v2/files/${parentId}`,
30
+ isRoot: false // Mock simplification
31
+ })),
32
+ version: file.version,
33
+ downloadUrl: `http://localhost/drive/v2/files/${file.id}?alt=media`
34
+ };
35
+ }
36
+ /**
37
+ * Maps a V2 API File Update/Insert body to a partial Internal DriveFile (V3 format).
38
+ */
39
+ function fromV2Update(body) {
40
+ const update = {};
41
+ if (typeof body.title === 'string')
42
+ update.name = body.title;
43
+ if (typeof body.mimeType === 'string')
44
+ update.mimeType = body.mimeType;
45
+ if (typeof body.modifiedDate === 'string')
46
+ update.modifiedTime = body.modifiedDate;
47
+ // Parents in V2 create are typically [{id: '...'}]
48
+ if (body.parents && Array.isArray(body.parents)) {
49
+ update.parents = body.parents
50
+ .map((p) => p.id)
51
+ .filter((id) => typeof id === 'string');
52
+ }
53
+ if (body.labels && typeof body.labels === 'object') {
54
+ const labels = body.labels;
55
+ if (typeof labels.starred === 'boolean')
56
+ update.starred = labels.starred;
57
+ if (typeof labels.trashed === 'boolean')
58
+ update.trashed = labels.trashed;
59
+ }
60
+ return update;
61
+ }
@@ -0,0 +1,2 @@
1
+ import { AppConfig } from '../types';
2
+ export declare const createV2Router: (config: AppConfig) => import("express-serve-static-core").Router;