web-manager 4.3.2 → 4.3.3
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/CLAUDE.md +3 -0
- package/docs/cdp-debugging.md +29 -0
- package/package.json +7 -1
- package/src/index.js +702 -0
- package/src/modules/analytics.js +250 -0
- package/src/modules/auth.js +420 -0
- package/src/modules/bindings.js +314 -0
- package/src/modules/dom.js +96 -0
- package/src/modules/firestore.js +306 -0
- package/src/modules/notifications.js +391 -0
- package/src/modules/sentry.js +199 -0
- package/src/modules/service-worker.js +196 -0
- package/src/modules/storage.js +133 -0
- package/src/modules/usage.js +264 -0
- package/src/modules/utilities.js +310 -0
- package/CHANGELOG.md +0 -242
- package/TODO.md +0 -59
- package/_legacy/helpers/auth-pages.js +0 -24
- package/_legacy/index.js +0 -1854
- package/_legacy/lib/account.js +0 -666
- package/_legacy/lib/debug.js +0 -56
- package/_legacy/lib/dom.js +0 -351
- package/_legacy/lib/require.js +0 -5
- package/_legacy/lib/storage.js +0 -78
- package/_legacy/lib/utilities.js +0 -180
- package/_legacy/service-worker copy.js +0 -347
- package/_legacy/test/test.js +0 -158
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
class Bindings {
|
|
2
|
+
constructor(manager) {
|
|
3
|
+
this.manager = manager;
|
|
4
|
+
this._context = {};
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// Update bindings with new data
|
|
8
|
+
update(data = {}) {
|
|
9
|
+
// Merge new data with existing context
|
|
10
|
+
// Whatever keys are provided will overwrite existing values
|
|
11
|
+
this._context = {
|
|
12
|
+
...this._context,
|
|
13
|
+
...data
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Get the top-level keys that were updating
|
|
17
|
+
const updatedKeys = Object.keys(data);
|
|
18
|
+
|
|
19
|
+
this._updateBindings(this._context, updatedKeys);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Get current context
|
|
23
|
+
getContext() {
|
|
24
|
+
return this._context;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Clear all context
|
|
28
|
+
clear() {
|
|
29
|
+
this._context = {};
|
|
30
|
+
this._updateBindings(this._context, null); // null = update all bindings
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Main binding update system
|
|
34
|
+
_updateBindings(context, updatedKeys = null) {
|
|
35
|
+
// Find all elements with data-wm-bind attribute
|
|
36
|
+
const bindElements = document.querySelectorAll('[data-wm-bind]');
|
|
37
|
+
|
|
38
|
+
/* @dev-only:start */
|
|
39
|
+
{
|
|
40
|
+
console.log('[Bindings] Updating bindings', context, updatedKeys);
|
|
41
|
+
}
|
|
42
|
+
/* @dev-only:end */
|
|
43
|
+
|
|
44
|
+
bindElements.forEach(element => {
|
|
45
|
+
const bindValue = element.getAttribute('data-wm-bind');
|
|
46
|
+
|
|
47
|
+
// Split by comma to support multiple actions
|
|
48
|
+
const bindings = this._parseBindings(bindValue);
|
|
49
|
+
|
|
50
|
+
// Execute each action, track if any were actually processed
|
|
51
|
+
let anyProcessed = false;
|
|
52
|
+
bindings.forEach(({ action, expression }) => {
|
|
53
|
+
if (this._executeAction(element, action, expression, context, updatedKeys)) {
|
|
54
|
+
anyProcessed = true;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Only remove skeleton if at least one binding was actually processed
|
|
59
|
+
if (!anyProcessed) return;
|
|
60
|
+
|
|
61
|
+
// Add bound class to trigger fade out
|
|
62
|
+
element.classList.add('wm-bound');
|
|
63
|
+
|
|
64
|
+
// Remove skeleton class after fade completes
|
|
65
|
+
setTimeout(() => {
|
|
66
|
+
element.classList.remove('wm-binding-skeleton');
|
|
67
|
+
}, 300);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Parse binding string into separate actions
|
|
72
|
+
_parseBindings(bindValue) {
|
|
73
|
+
const bindings = [];
|
|
74
|
+
|
|
75
|
+
// Split by comma, but be smart about it
|
|
76
|
+
// We need to handle cases where commas might be inside expressions
|
|
77
|
+
const parts = bindValue.split(',').map(p => p.trim());
|
|
78
|
+
|
|
79
|
+
parts.forEach(part => {
|
|
80
|
+
let action = '@text'; // Default action
|
|
81
|
+
let expression = part;
|
|
82
|
+
|
|
83
|
+
// Check if it starts with an action keyword
|
|
84
|
+
if (part.startsWith('@')) {
|
|
85
|
+
const spaceIndex = part.indexOf(' ');
|
|
86
|
+
if (spaceIndex > -1) {
|
|
87
|
+
action = part.slice(0, spaceIndex);
|
|
88
|
+
expression = part.slice(spaceIndex + 1).trim();
|
|
89
|
+
} else {
|
|
90
|
+
// No space means it's just an action with no expression (like @hide)
|
|
91
|
+
action = part;
|
|
92
|
+
expression = '';
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
bindings.push({ action, expression });
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return bindings;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Execute a single action on an element
|
|
103
|
+
// Returns true if the action was processed, false if skipped
|
|
104
|
+
_executeAction(element, action, expression, context, updatedKeys = null) {
|
|
105
|
+
switch (action) {
|
|
106
|
+
case '@show':
|
|
107
|
+
// Show element if condition is true (or always if no condition)
|
|
108
|
+
|
|
109
|
+
// Check if this path should be updated
|
|
110
|
+
if (!this._shouldUpdatePath(expression, updatedKeys)) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const shouldShow = expression ? this._evaluateCondition(expression, context) : true;
|
|
115
|
+
if (shouldShow) {
|
|
116
|
+
element.removeAttribute('hidden');
|
|
117
|
+
} else {
|
|
118
|
+
element.setAttribute('hidden', '');
|
|
119
|
+
}
|
|
120
|
+
return true;
|
|
121
|
+
|
|
122
|
+
case '@hide':
|
|
123
|
+
// Hide element if condition is true (or always if no condition)
|
|
124
|
+
|
|
125
|
+
// Check if this path should be updated
|
|
126
|
+
if (!this._shouldUpdatePath(expression, updatedKeys)) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const shouldHide = expression ? this._evaluateCondition(expression, context) : true;
|
|
131
|
+
if (shouldHide) {
|
|
132
|
+
element.setAttribute('hidden', '');
|
|
133
|
+
} else {
|
|
134
|
+
element.removeAttribute('hidden');
|
|
135
|
+
}
|
|
136
|
+
return true;
|
|
137
|
+
|
|
138
|
+
case '@attr':
|
|
139
|
+
// Set attribute value
|
|
140
|
+
// Format: @attr attributeName expression
|
|
141
|
+
const attrParts = expression.split(' ');
|
|
142
|
+
const attrName = attrParts[0];
|
|
143
|
+
const attrExpression = attrParts.slice(1).join(' ');
|
|
144
|
+
|
|
145
|
+
// Check if this path should be updated
|
|
146
|
+
if (!this._shouldUpdatePath(attrExpression, updatedKeys)) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const attrValue = this._resolvePath(context, attrExpression) || '';
|
|
151
|
+
|
|
152
|
+
if (attrValue) {
|
|
153
|
+
// Block javascript: protocol on URL attributes to prevent XSS
|
|
154
|
+
const URL_ATTRS = ['href', 'src', 'action', 'formaction'];
|
|
155
|
+
if (URL_ATTRS.includes(attrName.toLowerCase())
|
|
156
|
+
&& /^\s*javascript\s*:/i.test(String(attrValue))) {
|
|
157
|
+
console.warn(`[Bindings] Blocked javascript: URL in @attr ${attrName}`);
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
element.setAttribute(attrName, attrValue);
|
|
162
|
+
} else {
|
|
163
|
+
element.removeAttribute(attrName);
|
|
164
|
+
}
|
|
165
|
+
return true;
|
|
166
|
+
|
|
167
|
+
case '@style':
|
|
168
|
+
// Set CSS custom property or style
|
|
169
|
+
// Format: @style propertyName expression
|
|
170
|
+
const styleParts = expression.split(' ');
|
|
171
|
+
const styleName = styleParts[0];
|
|
172
|
+
const styleExpression = styleParts.slice(1).join(' ');
|
|
173
|
+
|
|
174
|
+
// Check if this path should be updated
|
|
175
|
+
if (!this._shouldUpdatePath(styleExpression, updatedKeys)) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const styleValue = this._resolvePath(context, styleExpression);
|
|
180
|
+
|
|
181
|
+
if (styleValue !== null && styleValue !== undefined && styleValue !== '') {
|
|
182
|
+
// If it starts with --, it's a CSS custom property
|
|
183
|
+
if (styleName.startsWith('--')) {
|
|
184
|
+
element.style.setProperty(styleName, styleValue);
|
|
185
|
+
} else {
|
|
186
|
+
// Regular style property
|
|
187
|
+
element.style[styleName] = styleValue;
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
// Remove the style if value is empty
|
|
191
|
+
if (styleName.startsWith('--')) {
|
|
192
|
+
element.style.removeProperty(styleName);
|
|
193
|
+
} else {
|
|
194
|
+
element.style[styleName] = '';
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return true;
|
|
198
|
+
|
|
199
|
+
case '@value':
|
|
200
|
+
// Set input/textarea value explicitly
|
|
201
|
+
// Check if this path should be updated
|
|
202
|
+
if (!this._shouldUpdatePath(expression, updatedKeys)) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const inputValue = this._resolvePath(context, expression) ?? '';
|
|
207
|
+
element.value = inputValue;
|
|
208
|
+
return true;
|
|
209
|
+
|
|
210
|
+
case '@text':
|
|
211
|
+
default:
|
|
212
|
+
// Set text content (default behavior)
|
|
213
|
+
|
|
214
|
+
// Check if this path should be updated
|
|
215
|
+
if (!this._shouldUpdatePath(expression, updatedKeys)) {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const textValue = this._resolvePath(context, expression) ?? '';
|
|
220
|
+
element.textContent = textValue;
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Check if a path should be updated based on updatedKeys
|
|
226
|
+
_shouldUpdatePath(path, updatedKeys) {
|
|
227
|
+
// If no updatedKeys filter, always update
|
|
228
|
+
if (updatedKeys === null || !path) {
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Strip negation operator if present before extracting root key
|
|
233
|
+
const cleanPath = path.trim().replace(/^!/, '');
|
|
234
|
+
|
|
235
|
+
// Extract the root key from the path
|
|
236
|
+
const rootKey = cleanPath.split('.')[0];
|
|
237
|
+
|
|
238
|
+
// Only update if the root key is in updatedKeys
|
|
239
|
+
return updatedKeys.includes(rootKey);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Resolve nested object path
|
|
243
|
+
_resolvePath(obj, path) {
|
|
244
|
+
if (!obj || !path) return null;
|
|
245
|
+
|
|
246
|
+
return path.split('.').reduce((current, key) => {
|
|
247
|
+
return current?.[key];
|
|
248
|
+
}, obj);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Safely evaluate simple conditions
|
|
252
|
+
_evaluateCondition(condition, context) {
|
|
253
|
+
try {
|
|
254
|
+
// Replace context references with actual values
|
|
255
|
+
// Support: auth.user.field, auth.account.field, simple comparisons
|
|
256
|
+
|
|
257
|
+
// Check for negation operator at the start
|
|
258
|
+
if (condition.trim().startsWith('!')) {
|
|
259
|
+
const expression = condition.trim().slice(1).trim();
|
|
260
|
+
const value = this._resolvePath(context, expression);
|
|
261
|
+
return !value;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Parse the condition to extract left side, operator, and right side
|
|
265
|
+
const comparisonMatch = condition.match(/^(.+?)\s*(===|!==|==|!=|>|<|>=|<=)\s*(.+)$/);
|
|
266
|
+
|
|
267
|
+
if (comparisonMatch) {
|
|
268
|
+
const [, leftPath, operator, rightValue] = comparisonMatch;
|
|
269
|
+
|
|
270
|
+
// Get the left side value
|
|
271
|
+
const leftValue = this._resolvePath(context, leftPath.trim());
|
|
272
|
+
|
|
273
|
+
// Parse the right side (could be string, number, boolean)
|
|
274
|
+
let right = rightValue.trim();
|
|
275
|
+
|
|
276
|
+
// Remove quotes if it's a string
|
|
277
|
+
if ((right.startsWith("'") && right.endsWith("'")) ||
|
|
278
|
+
(right.startsWith('"') && right.endsWith('"'))) {
|
|
279
|
+
right = right.slice(1, -1);
|
|
280
|
+
} else if (right === 'true') {
|
|
281
|
+
right = true;
|
|
282
|
+
} else if (right === 'false') {
|
|
283
|
+
right = false;
|
|
284
|
+
} else if (right === 'null') {
|
|
285
|
+
right = null;
|
|
286
|
+
} else if (!isNaN(right)) {
|
|
287
|
+
right = Number(right);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Evaluate based on operator
|
|
291
|
+
switch (operator) {
|
|
292
|
+
case '===': return leftValue === right;
|
|
293
|
+
case '!==': return leftValue !== right;
|
|
294
|
+
case '==': return leftValue == right;
|
|
295
|
+
case '!=': return leftValue != right;
|
|
296
|
+
case '>': return leftValue > right;
|
|
297
|
+
case '<': return leftValue < right;
|
|
298
|
+
case '>=': return leftValue >= right;
|
|
299
|
+
case '<=': return leftValue <= right;
|
|
300
|
+
default: return false;
|
|
301
|
+
}
|
|
302
|
+
} else {
|
|
303
|
+
// Simple truthy check (e.g., "auth.user.emailVerified" or "auth.account")
|
|
304
|
+
const value = this._resolvePath(context, condition.trim());
|
|
305
|
+
return !!value;
|
|
306
|
+
}
|
|
307
|
+
} catch (error) {
|
|
308
|
+
console.warn('Failed to evaluate condition:', condition, error);
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export default Bindings;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Load external script dynamically
|
|
2
|
+
export function loadScript(options) {
|
|
3
|
+
return new Promise((resolve, reject) => {
|
|
4
|
+
// Handle simple string parameter
|
|
5
|
+
if (typeof options === 'string') {
|
|
6
|
+
options = { src: options };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
src,
|
|
11
|
+
async = true,
|
|
12
|
+
defer = false,
|
|
13
|
+
crossorigin = false,
|
|
14
|
+
integrity = null,
|
|
15
|
+
attributes = {},
|
|
16
|
+
timeout = 60000,
|
|
17
|
+
retries = 0,
|
|
18
|
+
parent = null
|
|
19
|
+
} = options;
|
|
20
|
+
|
|
21
|
+
if (!src) {
|
|
22
|
+
return reject(new Error('Script source is required'));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let timeoutId;
|
|
26
|
+
let retryCount = 0;
|
|
27
|
+
|
|
28
|
+
function createAndLoadScript() {
|
|
29
|
+
const script = document.createElement('script');
|
|
30
|
+
script.src = src;
|
|
31
|
+
script.async = async;
|
|
32
|
+
script.defer = defer;
|
|
33
|
+
|
|
34
|
+
if (crossorigin) {
|
|
35
|
+
script.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (integrity) {
|
|
39
|
+
script.integrity = integrity;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Add custom attributes
|
|
43
|
+
Object.keys(attributes).forEach(name => {
|
|
44
|
+
script.setAttribute(name, attributes[name]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Set up timeout
|
|
48
|
+
if (timeout > 0) {
|
|
49
|
+
timeoutId = setTimeout(() => {
|
|
50
|
+
script.remove();
|
|
51
|
+
handleError(new Error(`Script load timeout: ${src}`));
|
|
52
|
+
}, timeout);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Event handlers
|
|
56
|
+
script.onload = () => {
|
|
57
|
+
clearTimeout(timeoutId);
|
|
58
|
+
resolve({ script, cached: false });
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
script.onerror = (error) => {
|
|
62
|
+
clearTimeout(timeoutId);
|
|
63
|
+
script.remove();
|
|
64
|
+
handleError(new Error(`Failed to load script ${src}`, { cause: error }));
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Append to document
|
|
68
|
+
const $targetParent = parent || document.head || document.documentElement;
|
|
69
|
+
$targetParent.appendChild(script);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function handleError(error) {
|
|
73
|
+
if (retryCount < retries) {
|
|
74
|
+
retryCount++;
|
|
75
|
+
setTimeout(createAndLoadScript, 1000 * retryCount);
|
|
76
|
+
} else {
|
|
77
|
+
reject(error);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
createAndLoadScript();
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Return promise that resolves when DOM is ready
|
|
86
|
+
export function ready() {
|
|
87
|
+
return new Promise((resolve) => {
|
|
88
|
+
if (document.readyState === 'loading') {
|
|
89
|
+
// Wait for DOM if still loading
|
|
90
|
+
document.addEventListener('DOMContentLoaded', resolve);
|
|
91
|
+
} else {
|
|
92
|
+
// DOM is already ready
|
|
93
|
+
resolve();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
class Firestore {
|
|
2
|
+
constructor(manager) {
|
|
3
|
+
this.manager = manager;
|
|
4
|
+
this._db = null;
|
|
5
|
+
this._initialized = false;
|
|
6
|
+
this._initPromise = null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async _ensureInitialized() {
|
|
10
|
+
if (this._initialized) {
|
|
11
|
+
return this._db;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (!this._initPromise) {
|
|
15
|
+
this._initPromise = this._initializeFirestore();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return this._initPromise;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async _initializeFirestore() {
|
|
22
|
+
try {
|
|
23
|
+
// Check if Firebase app is initialized
|
|
24
|
+
if (!this.manager._firebaseApp) {
|
|
25
|
+
throw new Error('Firebase app not initialized. Please initialize Firebase first.');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Dynamically import Firestore
|
|
29
|
+
const { getFirestore, doc: firestoreDoc, collection: firestoreCollection, getDoc, setDoc, updateDoc, deleteDoc, getDocs, query, where, orderBy, limit, startAt, endAt, onSnapshot } = await import('firebase/firestore');
|
|
30
|
+
|
|
31
|
+
// Store references for later use
|
|
32
|
+
this._firestoreMethods = {
|
|
33
|
+
doc: firestoreDoc,
|
|
34
|
+
collection: firestoreCollection,
|
|
35
|
+
getDoc,
|
|
36
|
+
setDoc,
|
|
37
|
+
updateDoc,
|
|
38
|
+
deleteDoc,
|
|
39
|
+
getDocs,
|
|
40
|
+
query,
|
|
41
|
+
where,
|
|
42
|
+
orderBy,
|
|
43
|
+
limit,
|
|
44
|
+
startAt,
|
|
45
|
+
endAt,
|
|
46
|
+
onSnapshot,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Reuse the Firestore instance already initialized in index.js
|
|
50
|
+
this._db = getFirestore(this.manager._firebaseApp);
|
|
51
|
+
|
|
52
|
+
// Connect to Firestore emulator in development
|
|
53
|
+
if (this.manager.isDevelopment() && this.manager.config.env?.FIREBASE_EMULATOR_CONNECT) {
|
|
54
|
+
console.log('[Firestore] Connecting to emulator at localhost:8080');
|
|
55
|
+
const { connectFirestoreEmulator } = await import('firebase/firestore');
|
|
56
|
+
connectFirestoreEmulator(this._db, 'localhost', 8080);
|
|
57
|
+
console.log('[Firestore] Emulator connected');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
this._initialized = true;
|
|
61
|
+
|
|
62
|
+
return this._db;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error('Failed to initialize Firestore:', error);
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Main doc() method - supports both 'path/to/doc' and ('collection', 'docId')
|
|
70
|
+
doc(...args) {
|
|
71
|
+
const self = this;
|
|
72
|
+
let docPath;
|
|
73
|
+
|
|
74
|
+
// Handle different argument patterns
|
|
75
|
+
if (args.length === 1 && typeof args[0] === 'string') {
|
|
76
|
+
// Single path string: doc('users/userId')
|
|
77
|
+
docPath = args[0];
|
|
78
|
+
} else if (args.length === 2 && typeof args[0] === 'string' && typeof args[1] === 'string') {
|
|
79
|
+
// Collection and doc ID: doc('users', 'userId')
|
|
80
|
+
docPath = `${args[0]}/${args[1]}`;
|
|
81
|
+
} else {
|
|
82
|
+
throw new Error('Invalid arguments for doc(). Use doc("path/to/doc") or doc("collection", "docId")');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
async get() {
|
|
87
|
+
await self._ensureInitialized();
|
|
88
|
+
const docRef = self._firestoreMethods.doc(self._db, docPath);
|
|
89
|
+
const docSnap = await self._firestoreMethods.getDoc(docRef);
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
exists: () => docSnap.exists(),
|
|
93
|
+
data: () => docSnap.data(),
|
|
94
|
+
id: docSnap.id,
|
|
95
|
+
ref: docRef
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
async set(data, options = {}) {
|
|
100
|
+
await self._ensureInitialized();
|
|
101
|
+
const docRef = self._firestoreMethods.doc(self._db, docPath);
|
|
102
|
+
return await self._firestoreMethods.setDoc(docRef, data, options);
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
async update(data) {
|
|
106
|
+
await self._ensureInitialized();
|
|
107
|
+
const docRef = self._firestoreMethods.doc(self._db, docPath);
|
|
108
|
+
return await self._firestoreMethods.updateDoc(docRef, data);
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
async delete() {
|
|
112
|
+
await self._ensureInitialized();
|
|
113
|
+
const docRef = self._firestoreMethods.doc(self._db, docPath);
|
|
114
|
+
return await self._firestoreMethods.deleteDoc(docRef);
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
onSnapshot(callback, errorCallback) {
|
|
118
|
+
let unsubscribe = function () {};
|
|
119
|
+
|
|
120
|
+
self._ensureInitialized().then(function () {
|
|
121
|
+
const docRef = self._firestoreMethods.doc(self._db, docPath);
|
|
122
|
+
|
|
123
|
+
unsubscribe = self._firestoreMethods.onSnapshot(docRef, function (docSnap) {
|
|
124
|
+
callback({
|
|
125
|
+
exists: () => docSnap.exists(),
|
|
126
|
+
data: () => docSnap.data(),
|
|
127
|
+
id: docSnap.id,
|
|
128
|
+
ref: docRef,
|
|
129
|
+
});
|
|
130
|
+
}, errorCallback);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return function () { unsubscribe(); };
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Collection method for queries
|
|
139
|
+
collection(collectionPath) {
|
|
140
|
+
const self = this;
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
async get() {
|
|
144
|
+
await self._ensureInitialized();
|
|
145
|
+
const collRef = self._firestoreMethods.collection(self._db, collectionPath);
|
|
146
|
+
const querySnapshot = await self._firestoreMethods.getDocs(collRef);
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
docs: querySnapshot.docs.map(doc => ({
|
|
150
|
+
id: doc.id,
|
|
151
|
+
data: () => doc.data(),
|
|
152
|
+
exists: () => doc.exists(),
|
|
153
|
+
ref: doc.ref
|
|
154
|
+
})),
|
|
155
|
+
size: querySnapshot.size,
|
|
156
|
+
empty: querySnapshot.empty,
|
|
157
|
+
forEach: (callback) => querySnapshot.forEach(callback)
|
|
158
|
+
};
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
where(field, operator, value) {
|
|
162
|
+
return self._buildQuery(collectionPath, [{ type: 'where', field, operator, value }]);
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
orderBy(field, direction = 'asc') {
|
|
166
|
+
return self._buildQuery(collectionPath, [{ type: 'orderBy', field, direction }]);
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
limit(count) {
|
|
170
|
+
return self._buildQuery(collectionPath, [{ type: 'limit', count }]);
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
doc(docId) {
|
|
174
|
+
if (docId) {
|
|
175
|
+
return self.doc(`${collectionPath}/${docId}`);
|
|
176
|
+
}
|
|
177
|
+
// Auto-generate ID if not provided
|
|
178
|
+
return self.doc(`${collectionPath}/${self._generateId()}`);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Build chainable query
|
|
184
|
+
_buildQuery(collectionPath, constraints = []) {
|
|
185
|
+
const self = this;
|
|
186
|
+
|
|
187
|
+
const queryBuilder = {
|
|
188
|
+
where(field, operator, value) {
|
|
189
|
+
constraints.push({ type: 'where', field, operator, value });
|
|
190
|
+
return queryBuilder;
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
orderBy(field, direction = 'asc') {
|
|
194
|
+
constraints.push({ type: 'orderBy', field, direction });
|
|
195
|
+
return queryBuilder;
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
limit(count) {
|
|
199
|
+
constraints.push({ type: 'limit', count });
|
|
200
|
+
return queryBuilder;
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
startAt(...values) {
|
|
204
|
+
constraints.push({ type: 'startAt', values });
|
|
205
|
+
return queryBuilder;
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
endAt(...values) {
|
|
209
|
+
constraints.push({ type: 'endAt', values });
|
|
210
|
+
return queryBuilder;
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
async get() {
|
|
214
|
+
return self._executeQuery(collectionPath, constraints);
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
onSnapshot(callback, errorCallback) {
|
|
218
|
+
let unsubscribe = function () {};
|
|
219
|
+
|
|
220
|
+
self._ensureInitialized().then(function () {
|
|
221
|
+
const collRef = self._firestoreMethods.collection(self._db, collectionPath);
|
|
222
|
+
const queryConstraints = self._buildConstraints(constraints);
|
|
223
|
+
const q = self._firestoreMethods.query(collRef, ...queryConstraints);
|
|
224
|
+
|
|
225
|
+
unsubscribe = self._firestoreMethods.onSnapshot(q, function (querySnapshot) {
|
|
226
|
+
callback({
|
|
227
|
+
docs: querySnapshot.docs.map(doc => ({
|
|
228
|
+
id: doc.id,
|
|
229
|
+
data: () => doc.data(),
|
|
230
|
+
exists: () => doc.exists(),
|
|
231
|
+
ref: doc.ref,
|
|
232
|
+
})),
|
|
233
|
+
size: querySnapshot.size,
|
|
234
|
+
empty: querySnapshot.empty,
|
|
235
|
+
forEach: (cb) => querySnapshot.forEach(cb),
|
|
236
|
+
});
|
|
237
|
+
}, errorCallback);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
return function () { unsubscribe(); };
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
return queryBuilder;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Build query constraint objects from constraint descriptors
|
|
248
|
+
_buildConstraints(constraints) {
|
|
249
|
+
const queryConstraints = [];
|
|
250
|
+
|
|
251
|
+
for (const constraint of constraints) {
|
|
252
|
+
switch (constraint.type) {
|
|
253
|
+
case 'where':
|
|
254
|
+
queryConstraints.push(this._firestoreMethods.where(constraint.field, constraint.operator, constraint.value));
|
|
255
|
+
break;
|
|
256
|
+
case 'orderBy':
|
|
257
|
+
queryConstraints.push(this._firestoreMethods.orderBy(constraint.field, constraint.direction));
|
|
258
|
+
break;
|
|
259
|
+
case 'limit':
|
|
260
|
+
queryConstraints.push(this._firestoreMethods.limit(constraint.count));
|
|
261
|
+
break;
|
|
262
|
+
case 'startAt':
|
|
263
|
+
queryConstraints.push(this._firestoreMethods.startAt(...constraint.values));
|
|
264
|
+
break;
|
|
265
|
+
case 'endAt':
|
|
266
|
+
queryConstraints.push(this._firestoreMethods.endAt(...constraint.values));
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return queryConstraints;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Execute a query and return formatted results
|
|
275
|
+
async _executeQuery(collectionPath, constraints) {
|
|
276
|
+
await this._ensureInitialized();
|
|
277
|
+
const collRef = this._firestoreMethods.collection(this._db, collectionPath);
|
|
278
|
+
const queryConstraints = this._buildConstraints(constraints);
|
|
279
|
+
const q = this._firestoreMethods.query(collRef, ...queryConstraints);
|
|
280
|
+
const querySnapshot = await this._firestoreMethods.getDocs(q);
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
docs: querySnapshot.docs.map(doc => ({
|
|
284
|
+
id: doc.id,
|
|
285
|
+
data: () => doc.data(),
|
|
286
|
+
exists: () => doc.exists(),
|
|
287
|
+
ref: doc.ref,
|
|
288
|
+
})),
|
|
289
|
+
size: querySnapshot.size,
|
|
290
|
+
empty: querySnapshot.empty,
|
|
291
|
+
forEach: (callback) => querySnapshot.forEach(callback),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Helper to generate document IDs (similar to Firebase auto-generated IDs)
|
|
296
|
+
_generateId() {
|
|
297
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
298
|
+
let id = '';
|
|
299
|
+
for (let i = 0; i < 20; i++) {
|
|
300
|
+
id += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
301
|
+
}
|
|
302
|
+
return id;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export default Firestore;
|