@open-webapp/drive-sync 0.1.0 → 0.3.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/README.md +1 -0
- package/SPEC.md +9 -4
- package/dist/connection.d.ts +24 -0
- package/dist/connection.js +31 -0
- package/dist/errors.d.ts +28 -0
- package/dist/errors.js +31 -0
- package/dist/files.d.ts +24 -1
- package/dist/files.js +170 -12
- package/dist/index.d.ts +20 -2
- package/dist/index.js +39 -1
- package/dist/picker.d.ts +95 -0
- package/dist/picker.js +140 -0
- package/dist/storage.d.ts +15 -1
- package/dist/storage.js +30 -0
- package/dist/testing/driveFake.d.ts +10 -0
- package/dist/testing/driveFake.js +12 -1
- package/dist/testing/gisFake.d.ts +6 -0
- package/dist/testing/gisFake.js +13 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +1 -0
- package/dist/testing/pickerFake.d.ts +46 -0
- package/dist/testing/pickerFake.js +189 -0
- package/dist/token.js +11 -0
- package/dist/types.d.ts +42 -0
- package/package.json +1 -1
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A scriptable double for Google Picker (google.picker),
|
|
3
|
+
* for use in tests of code that calls `window.google.picker.PickerBuilder`
|
|
4
|
+
* and related APIs.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* const pickerFake = createPickerFake()
|
|
10
|
+
* pickerFake.install()
|
|
11
|
+
* // ... exercise code under test that calls openPicker() ...
|
|
12
|
+
* pickerFake.simulatePick([{ fileId: '123', name: 'doc.txt', mimeType: 'text/plain' }])
|
|
13
|
+
* await expect(openPickerPromise).resolves.toEqual([...])
|
|
14
|
+
* expect(pickerFake.calls).toHaveLength(1)
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export function createPickerFake() {
|
|
18
|
+
const calls = [];
|
|
19
|
+
let storedCallback = null;
|
|
20
|
+
let previousGapi;
|
|
21
|
+
let hadGapi = false;
|
|
22
|
+
let previousGoogle;
|
|
23
|
+
let hadGoogle = false;
|
|
24
|
+
let previousGooglePicker;
|
|
25
|
+
let hadGooglePicker = false;
|
|
26
|
+
/**
|
|
27
|
+
* Fake DocsView class for stubbing window.google.picker.DocsView.
|
|
28
|
+
* Stores MIME types and parent folder ID, and is chainable.
|
|
29
|
+
*/
|
|
30
|
+
class FakeDocsView {
|
|
31
|
+
mimeTypes;
|
|
32
|
+
parentId;
|
|
33
|
+
setMimeTypes(types) {
|
|
34
|
+
this.mimeTypes = types;
|
|
35
|
+
return this;
|
|
36
|
+
}
|
|
37
|
+
setParent(folderId) {
|
|
38
|
+
this.parentId = folderId;
|
|
39
|
+
return this;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Fake PickerInstance (returned by PickerBuilder.build()).
|
|
44
|
+
* When setVisible(true) is called, it's recorded but callback is not fired
|
|
45
|
+
* until simulatePick or simulateCancel is called.
|
|
46
|
+
*/
|
|
47
|
+
class FakePickerInstance {
|
|
48
|
+
setVisible(visible) {
|
|
49
|
+
// Visibility change is recorded implicitly by the builder construction.
|
|
50
|
+
// The callback will be triggered by simulatePick/simulateCancel.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Fake PickerBuilder class for stubbing window.google.picker.PickerBuilder.
|
|
55
|
+
* Chainable methods all return `this`, and build() returns a FakePickerInstance.
|
|
56
|
+
*/
|
|
57
|
+
class FakePickerBuilder {
|
|
58
|
+
currentCall = {
|
|
59
|
+
views: [],
|
|
60
|
+
features: [],
|
|
61
|
+
};
|
|
62
|
+
addView(view) {
|
|
63
|
+
this.currentCall.views.push({
|
|
64
|
+
mimeTypes: view.mimeTypes,
|
|
65
|
+
parentId: view.parentId,
|
|
66
|
+
});
|
|
67
|
+
return this;
|
|
68
|
+
}
|
|
69
|
+
setOAuthToken(token) {
|
|
70
|
+
this.currentCall.oauthToken = token;
|
|
71
|
+
return this;
|
|
72
|
+
}
|
|
73
|
+
setDeveloperKey(key) {
|
|
74
|
+
this.currentCall.developerKey = key;
|
|
75
|
+
return this;
|
|
76
|
+
}
|
|
77
|
+
setAppId(appId) {
|
|
78
|
+
this.currentCall.appId = appId;
|
|
79
|
+
return this;
|
|
80
|
+
}
|
|
81
|
+
enableFeature(feature) {
|
|
82
|
+
this.currentCall.features.push(feature);
|
|
83
|
+
return this;
|
|
84
|
+
}
|
|
85
|
+
setCallback(callback) {
|
|
86
|
+
storedCallback = callback;
|
|
87
|
+
return this;
|
|
88
|
+
}
|
|
89
|
+
build() {
|
|
90
|
+
// Record the call once build() is invoked (not before, so all config is captured)
|
|
91
|
+
calls.push(this.currentCall);
|
|
92
|
+
return new FakePickerInstance();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
calls,
|
|
97
|
+
simulatePick(files) {
|
|
98
|
+
if (!storedCallback) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const callback = storedCallback;
|
|
102
|
+
queueMicrotask(() => {
|
|
103
|
+
callback({
|
|
104
|
+
action: 'picked',
|
|
105
|
+
docs: files.map((file) => ({
|
|
106
|
+
id: file.fileId,
|
|
107
|
+
name: file.name,
|
|
108
|
+
mimeType: file.mimeType,
|
|
109
|
+
})),
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
},
|
|
113
|
+
simulateCancel() {
|
|
114
|
+
if (!storedCallback) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const callback = storedCallback;
|
|
118
|
+
queueMicrotask(() => {
|
|
119
|
+
callback({
|
|
120
|
+
action: 'cancel',
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
},
|
|
124
|
+
install() {
|
|
125
|
+
const w = globalThis;
|
|
126
|
+
// Save and replace window.gapi
|
|
127
|
+
hadGapi = Object.prototype.hasOwnProperty.call(w, 'gapi');
|
|
128
|
+
previousGapi = w.gapi;
|
|
129
|
+
w.gapi = {
|
|
130
|
+
load(api, callback) {
|
|
131
|
+
if (api === 'picker') {
|
|
132
|
+
queueMicrotask(callback);
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
// Save and replace window.google
|
|
137
|
+
hadGoogle = Object.prototype.hasOwnProperty.call(w, 'google');
|
|
138
|
+
previousGoogle = w.google;
|
|
139
|
+
if (!w.google) {
|
|
140
|
+
w.google = {};
|
|
141
|
+
}
|
|
142
|
+
// Save and replace window.google.picker
|
|
143
|
+
hadGooglePicker = Object.prototype.hasOwnProperty.call(w.google, 'picker');
|
|
144
|
+
previousGooglePicker = w.google.picker;
|
|
145
|
+
w.google.picker = {
|
|
146
|
+
PickerBuilder: FakePickerBuilder,
|
|
147
|
+
DocsView: FakeDocsView,
|
|
148
|
+
Action: {
|
|
149
|
+
PICKED: 'picked',
|
|
150
|
+
CANCEL: 'cancel',
|
|
151
|
+
},
|
|
152
|
+
ViewId: {
|
|
153
|
+
DOCS: 'docs',
|
|
154
|
+
},
|
|
155
|
+
Feature: {
|
|
156
|
+
MULTISELECT_ENABLED: 'multiselect',
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
},
|
|
160
|
+
uninstall() {
|
|
161
|
+
const w = globalThis;
|
|
162
|
+
// Restore window.gapi
|
|
163
|
+
if (hadGapi) {
|
|
164
|
+
w.gapi = previousGapi;
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
delete w.gapi;
|
|
168
|
+
}
|
|
169
|
+
// Restore window.google.picker
|
|
170
|
+
if (hadGooglePicker) {
|
|
171
|
+
w.google.picker = previousGooglePicker;
|
|
172
|
+
}
|
|
173
|
+
else if (w.google) {
|
|
174
|
+
delete w.google.picker;
|
|
175
|
+
}
|
|
176
|
+
// Restore window.google (if it was only created by install())
|
|
177
|
+
if (!hadGoogle) {
|
|
178
|
+
delete w.google;
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
w.google = previousGoogle;
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
reset() {
|
|
185
|
+
calls.length = 0;
|
|
186
|
+
storedCallback = null;
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
package/dist/token.js
CHANGED
|
@@ -114,6 +114,17 @@ async function acquireTokenUncoalesced(opts) {
|
|
|
114
114
|
}
|
|
115
115
|
resolve(res);
|
|
116
116
|
},
|
|
117
|
+
// Without this, a popup that the browser blocks or the user closes
|
|
118
|
+
// settles NOTHING: GIS reports those through error_callback only, so
|
|
119
|
+
// the promise below would stay pending forever and every awaiting
|
|
120
|
+
// Drive call would hang until the caller's own timeout (if any).
|
|
121
|
+
error_callback: (err) => {
|
|
122
|
+
reject(new NeedsReauthError(err?.type === 'popup_failed_to_open'
|
|
123
|
+
? 'Google sign-in popup was blocked by the browser'
|
|
124
|
+
: err?.type === 'popup_closed'
|
|
125
|
+
? 'Google sign-in popup was closed before completing'
|
|
126
|
+
: `Google sign-in failed: ${err?.type ?? 'unknown error'}`, { reason: err?.type ?? 'gis_error' }));
|
|
127
|
+
},
|
|
117
128
|
});
|
|
118
129
|
opts.logger?.debug('drive-sync: requesting access token', {
|
|
119
130
|
projectId: opts.projectId,
|
package/dist/types.d.ts
CHANGED
|
@@ -27,6 +27,26 @@ export interface StoredToken {
|
|
|
27
27
|
export interface FileRef {
|
|
28
28
|
id: string;
|
|
29
29
|
name?: string;
|
|
30
|
+
/** Drive's monotonic change counter, when the call requested it. */
|
|
31
|
+
version?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Sync state of one file relative to what this client last restored.
|
|
35
|
+
* Returned by `files.status()`.
|
|
36
|
+
*/
|
|
37
|
+
export interface FileState {
|
|
38
|
+
fileId: string;
|
|
39
|
+
/** False when the file is missing or not visible to the connected account. */
|
|
40
|
+
exists: boolean;
|
|
41
|
+
/** Version last restored/written by this client; null if never. */
|
|
42
|
+
baseVersion: string | null;
|
|
43
|
+
/** Version currently on Drive; null when the file does not exist. */
|
|
44
|
+
remoteVersion: string | null;
|
|
45
|
+
/** True when a write would be refused with a RemoteChangedError. */
|
|
46
|
+
changedSinceRestore: boolean;
|
|
47
|
+
remoteModifiedTime?: string;
|
|
48
|
+
/** Epoch ms of the last restore/write by this client; null if never. */
|
|
49
|
+
lastRestoredAt: number | null;
|
|
30
50
|
}
|
|
31
51
|
/** Drive permission shape as returned/accepted by the Drive Permissions API. */
|
|
32
52
|
export interface DrivePermission {
|
|
@@ -35,6 +55,28 @@ export interface DrivePermission {
|
|
|
35
55
|
role: string;
|
|
36
56
|
emailAddress?: string;
|
|
37
57
|
}
|
|
58
|
+
/** Google Workspace MIME type shorthands for common document types. */
|
|
59
|
+
export type WorkspaceMimeShorthand = 'docs' | 'sheets' | 'slides' | 'forms' | 'drawings';
|
|
60
|
+
/** Options for opening the Google Picker to select files. */
|
|
61
|
+
export interface PickFileOptions {
|
|
62
|
+
apiKey: string;
|
|
63
|
+
/**
|
|
64
|
+
* Cloud project *number* of the OAuth client configured on
|
|
65
|
+
* `DriveSyncOptions.clientId`. Passed to Picker's `setAppId()`; required
|
|
66
|
+
* because drive-sync only ever holds a `drive.file`-scoped token.
|
|
67
|
+
*/
|
|
68
|
+
appId: string;
|
|
69
|
+
mimeTypes?: (string | WorkspaceMimeShorthand)[];
|
|
70
|
+
multiSelect?: boolean;
|
|
71
|
+
parentFolderId?: string;
|
|
72
|
+
}
|
|
73
|
+
/** File information returned after selection from Google Picker. */
|
|
74
|
+
export interface PickedFile {
|
|
75
|
+
fileId: string;
|
|
76
|
+
name: string;
|
|
77
|
+
mimeType: string;
|
|
78
|
+
content: string | Blob | null;
|
|
79
|
+
}
|
|
38
80
|
/** Options accepted on every Drive-op call site. */
|
|
39
81
|
export interface CallOptions {
|
|
40
82
|
/** Whether an interactive (popup/redirect) auth flow may be triggered. Defaults to false. */
|