@docstack/pouchdb-adapter-googledrive 0.0.5 → 0.0.6

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/lib/client.d.ts CHANGED
@@ -4,6 +4,7 @@ export interface DriveFile {
4
4
  mimeType: string;
5
5
  parents?: string[];
6
6
  etag?: string;
7
+ modifiedTime?: string;
7
8
  }
8
9
  export interface DriveClientOptions {
9
10
  accessToken: string | (() => Promise<string>);
@@ -19,10 +20,12 @@ export declare class GoogleDriveClient {
19
20
  createFile(name: string, parents: string[] | undefined, mimeType: string, content: string): Promise<{
20
21
  id: string;
21
22
  etag: string;
23
+ modifiedTime: string;
22
24
  }>;
23
25
  updateFile(fileId: string, content: string, expectedEtag?: string): Promise<{
24
26
  id: string;
25
27
  etag: string;
28
+ modifiedTime: string;
26
29
  }>;
27
30
  deleteFile(fileId: string): Promise<void>;
28
31
  private buildMultipart;
package/lib/client.js CHANGED
@@ -14,24 +14,44 @@ class GoogleDriveClient {
14
14
  return this.options.accessToken;
15
15
  }
16
16
  async fetch(url, init) {
17
+ const method = init.method || 'GET';
17
18
  const token = await this.getToken();
18
19
  const headers = new Headers(init.headers);
19
20
  headers.set('Authorization', `Bearer ${token}`);
20
- const res = await fetch(url, { ...init, headers });
21
- const method = init.method || 'GET';
21
+ let res;
22
+ try {
23
+ res = await fetch(url, { ...init, headers });
24
+ }
25
+ catch (networkErr) {
26
+ const err = new Error(`Network Error: ${networkErr.message} (${method} ${url})`);
27
+ err.code = 'network_error';
28
+ err.url = url;
29
+ err.method = method;
30
+ throw err;
31
+ }
22
32
  if (!res.ok) {
23
- // Basic error handling
24
33
  const text = await res.text();
25
34
  let errorMsg = `Drive API Error: ${res.status} ${res.statusText} (${method} ${url})`;
35
+ let reason = res.statusText;
26
36
  try {
27
37
  const json = JSON.parse(text);
28
- if (json.error && json.error.message) {
29
- errorMsg += ` - ${json.error.message}`;
38
+ const gError = json.error;
39
+ if (gError) {
40
+ errorMsg += ` - ${gError.message || 'Unknown Error'}`;
41
+ if (Array.isArray(gError.errors) && gError.errors.length > 0) {
42
+ reason = gError.errors[0].reason || reason;
43
+ if (gError.errors[0].message && gError.errors[0].message !== gError.message) {
44
+ errorMsg += ` (${gError.errors[0].message})`;
45
+ }
46
+ }
30
47
  }
31
48
  }
32
49
  catch { }
33
50
  const err = new Error(errorMsg);
34
51
  err.status = res.status;
52
+ err.code = reason;
53
+ err.url = url;
54
+ err.method = method;
35
55
  throw err;
36
56
  }
37
57
  return res;
@@ -39,11 +59,11 @@ class GoogleDriveClient {
39
59
  async listFiles(q) {
40
60
  const params = new URLSearchParams({
41
61
  q,
42
- fields: 'files(id, name, mimeType, parents, etag)',
43
- spaces: 'drive',
44
- pageSize: '1000' // Ensure we get enough
62
+ fields: 'files(id,name,mimeType,parents,modifiedTime)'
45
63
  });
46
- const res = await this.fetch(`${BASE_URL}?${params.toString()}`, { method: 'GET' });
64
+ // FIX: URLSearchParams uses '+', but Drive API is safer with '%20'
65
+ const queryString = params.toString().replace(/\+/g, '%20');
66
+ const res = await this.fetch(`${BASE_URL}?${queryString}`, { method: 'GET' });
47
67
  const data = await res.json();
48
68
  return data.files || [];
49
69
  }
@@ -51,7 +71,8 @@ class GoogleDriveClient {
51
71
  // Try getting media
52
72
  try {
53
73
  const params = new URLSearchParams({ alt: 'media' });
54
- const res = await this.fetch(`${BASE_URL}/${fileId}?${params.toString()}`, { method: 'GET' });
74
+ const queryString = params.toString().replace(/\+/g, '%20');
75
+ const res = await this.fetch(`${BASE_URL}/${fileId}?${queryString}`, { method: 'GET' });
55
76
  // Standard fetch handles JSON/Text transparency?
56
77
  // We expect JSON mostly, but sometimes we might want text.
57
78
  // PouchDB adapter flow: downloadJson, downloadNdjson
@@ -70,8 +91,9 @@ class GoogleDriveClient {
70
91
  }
71
92
  // Single metadata get (for etag check)
72
93
  async getFileMetadata(fileId) {
73
- const params = new URLSearchParams({ fields: 'id, name, mimeType, parents, etag' });
74
- const res = await this.fetch(`${BASE_URL}/${fileId}?${params.toString()}`, { method: 'GET' });
94
+ const params = new URLSearchParams({ fields: 'id,name,mimeType,parents,modifiedTime' });
95
+ const queryString = params.toString().replace(/\+/g, '%20');
96
+ const res = await this.fetch(`${BASE_URL}/${fileId}?${queryString}`, { method: 'GET' });
75
97
  return await res.json();
76
98
  }
77
99
  async createFile(name, parents, mimeType, content) {
@@ -82,32 +104,47 @@ class GoogleDriveClient {
82
104
  };
83
105
  // Folders or empty content can use simple metadata-only POST
84
106
  if (!content && mimeType === 'application/vnd.google-apps.folder') {
85
- const res = await this.fetch(`${BASE_URL}?fields=id,etag`, {
107
+ const res = await this.fetch(`${BASE_URL}?fields=id,modifiedTime`, {
86
108
  method: 'POST',
87
109
  headers: { 'Content-Type': 'application/json' },
88
110
  body: JSON.stringify(metadata)
89
111
  });
90
- return await res.json();
112
+ const data = await res.json();
113
+ return {
114
+ id: data.id,
115
+ etag: data.etag || '',
116
+ modifiedTime: data.modifiedTime || ''
117
+ };
91
118
  }
92
119
  const multipartBody = this.buildMultipart(metadata, content, mimeType);
93
- const res = await this.fetch(`${UPLOAD_URL}?uploadType=multipart&fields=id,etag`, {
120
+ const res = await this.fetch(`${UPLOAD_URL}?uploadType=multipart&fields=id,modifiedTime`, {
94
121
  method: 'POST',
95
122
  headers: {
96
123
  'Content-Type': `multipart/related; boundary=${multipartBody.boundary}`
97
124
  },
98
125
  body: multipartBody.body
99
126
  });
100
- return await res.json();
127
+ const data = await res.json();
128
+ return {
129
+ id: data.id,
130
+ etag: data.etag || '',
131
+ modifiedTime: data.modifiedTime || ''
132
+ };
101
133
  }
102
134
  async updateFile(fileId, content, expectedEtag) {
103
135
  // Update content (media) usually, but sometimes meta?
104
136
  // In our usage (saveMeta), we update body.
105
- const res = await this.fetch(`${UPLOAD_URL}/${fileId}?uploadType=media&fields=id,etag`, {
137
+ const res = await this.fetch(`${UPLOAD_URL}/${fileId}?uploadType=media&fields=id,modifiedTime`, {
106
138
  method: 'PATCH',
107
139
  headers: expectedEtag ? { 'If-Match': expectedEtag, 'Content-Type': 'application/json' } : { 'Content-Type': 'application/json' },
108
140
  body: content
109
141
  });
110
- return await res.json();
142
+ const data = await res.json();
143
+ return {
144
+ id: data.id,
145
+ etag: data.etag || '',
146
+ modifiedTime: data.modifiedTime || ''
147
+ };
111
148
  }
112
149
  async deleteFile(fileId) {
113
150
  await this.fetch(`${BASE_URL}/${fileId}`, { method: 'DELETE' });
package/lib/drive.d.ts CHANGED
@@ -19,6 +19,7 @@ export declare class DriveHandler {
19
19
  private compactionSizeThreshold;
20
20
  private meta;
21
21
  private metaEtag;
22
+ private metaModifiedTime;
22
23
  private index;
23
24
  private docCache;
24
25
  private pendingChanges;
package/lib/drive.js CHANGED
@@ -27,6 +27,7 @@ class DriveHandler {
27
27
  dbName: ''
28
28
  };
29
29
  this.metaEtag = null;
30
+ this.metaModifiedTime = null;
30
31
  // In-Memory Index: ID -> Metadata/Pointer
31
32
  this.index = {};
32
33
  this.pendingChanges = [];
@@ -57,6 +58,7 @@ class DriveHandler {
57
58
  if (metaFile) {
58
59
  this.meta = await this.downloadJson(metaFile.id);
59
60
  this.metaEtag = metaFile.etag || null;
61
+ this.metaModifiedTime = metaFile.modifiedTime || null;
60
62
  }
61
63
  else {
62
64
  await this.saveMeta(this.meta);
@@ -438,7 +440,11 @@ class DriveHandler {
438
440
  const q = `name = '${safeName}' and '${this.folderId}' in parents and trashed = false`;
439
441
  const files = await this.client.listFiles(q);
440
442
  if (files.length > 0)
441
- return { id: files[0].id, etag: files[0].etag || '' };
443
+ return {
444
+ id: files[0].id,
445
+ etag: files[0].etag || '',
446
+ modifiedTime: files[0].modifiedTime || ''
447
+ };
442
448
  return null;
443
449
  }
444
450
  async downloadJson(fileId) {
@@ -472,10 +478,12 @@ class DriveHandler {
472
478
  if (metaFile) {
473
479
  const res = await this.client.updateFile(metaFile.id, content, expectedEtag || undefined);
474
480
  this.metaEtag = res.etag;
481
+ this.metaModifiedTime = res.modifiedTime;
475
482
  }
476
483
  else {
477
484
  const res = await this.client.createFile('_meta.json', [this.folderId], 'application/json', content);
478
485
  this.metaEtag = res.etag;
486
+ this.metaModifiedTime = res.modifiedTime;
479
487
  }
480
488
  }
481
489
  async countTotalChanges() {
@@ -506,8 +514,8 @@ class DriveHandler {
506
514
  const metaFile = await this.findFile('_meta.json');
507
515
  if (!metaFile)
508
516
  return;
509
- // Etag check
510
- if (metaFile.etag !== this.metaEtag) {
517
+ // Use modifiedTime for polling as it's readable in projections
518
+ if (metaFile.modifiedTime !== this.metaModifiedTime) {
511
519
  await this.load();
512
520
  this.notifyListeners();
513
521
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docstack/pouchdb-adapter-googledrive",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "description": "PouchDB adapter for Google Drive",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",