@sneat/api 0.1.1
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/eslint.config.js +7 -0
- package/ng-package.json +7 -0
- package/package.json +16 -0
- package/project.json +38 -0
- package/src/index.ts +5 -0
- package/src/lib/sneat-api-service-factory.spec.ts +132 -0
- package/src/lib/sneat-api-service-factory.ts +59 -0
- package/src/lib/sneat-api-service.interface.ts +50 -0
- package/src/lib/sneat-api-service.spec.ts +298 -0
- package/src/lib/sneat-api-service.ts +138 -0
- package/src/lib/sneat-api.module.spec.ts +18 -0
- package/src/lib/sneat-api.module.ts +10 -0
- package/src/lib/sneat-firestore.service.spec.ts +465 -0
- package/src/lib/sneat-firestore.service.ts +157 -0
- package/src/test-setup.ts +3 -0
- package/tsconfig.json +13 -0
- package/tsconfig.lib.json +19 -0
- package/tsconfig.lib.prod.json +7 -0
- package/tsconfig.spec.json +31 -0
- package/vite.config.mts +10 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
|
|
2
|
+
import {
|
|
3
|
+
Inject,
|
|
4
|
+
Injectable,
|
|
5
|
+
InjectionToken,
|
|
6
|
+
OnDestroy,
|
|
7
|
+
Optional,
|
|
8
|
+
} from '@angular/core';
|
|
9
|
+
import { Auth, onIdTokenChanged } from '@angular/fire/auth';
|
|
10
|
+
import { Observable, Subject, throwError } from 'rxjs';
|
|
11
|
+
import { ISneatApiService } from './sneat-api-service.interface';
|
|
12
|
+
|
|
13
|
+
const userIsNotAuthenticatedNoFirebaseToken =
|
|
14
|
+
'User is not authenticated yet - no Firebase ID token';
|
|
15
|
+
|
|
16
|
+
export const SneatApiAuthTokenProvider = new InjectionToken(
|
|
17
|
+
'SneatApiAuthTokenProvider',
|
|
18
|
+
);
|
|
19
|
+
export const SneatApiBaseUrl = new InjectionToken('SneatApiBaseUrl');
|
|
20
|
+
export const DefaultSneatAppApiBaseUrl = 'https://api.sneat.ws/v0/';
|
|
21
|
+
|
|
22
|
+
@Injectable({ providedIn: 'root' }) // Should it be in root? Probably it is OK.
|
|
23
|
+
export class SneatApiService implements ISneatApiService, OnDestroy {
|
|
24
|
+
private readonly baseUrl: string;
|
|
25
|
+
|
|
26
|
+
private readonly destroyed = new Subject<void>();
|
|
27
|
+
private authToken?: string;
|
|
28
|
+
|
|
29
|
+
constructor(
|
|
30
|
+
private readonly httpClient: HttpClient,
|
|
31
|
+
@Inject(SneatApiBaseUrl) @Optional() baseUrl: string | null,
|
|
32
|
+
private readonly afAuth: Auth,
|
|
33
|
+
) {
|
|
34
|
+
this.baseUrl = baseUrl ?? DefaultSneatAppApiBaseUrl;
|
|
35
|
+
// console.log('SneatApiService.constructor()', this.baseUrl);
|
|
36
|
+
onIdTokenChanged(this.afAuth, {
|
|
37
|
+
next: (user) => {
|
|
38
|
+
user
|
|
39
|
+
?.getIdToken()
|
|
40
|
+
.then(this.setApiAuthToken)
|
|
41
|
+
.catch((err) => console.error('getIdToken() error:', err));
|
|
42
|
+
},
|
|
43
|
+
error: (error) => {
|
|
44
|
+
console.error('onIdTokenChanged() error:', error);
|
|
45
|
+
},
|
|
46
|
+
complete: () => void 0,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
ngOnDestroy() {
|
|
51
|
+
this.destroyed.next();
|
|
52
|
+
this.destroyed.complete();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// TODO: It's made public because we use it in Login page, might be a bad idea consider making private and rely on afAuth.idToken event
|
|
56
|
+
setApiAuthToken = (token?: string) => {
|
|
57
|
+
// console.log('setApiAuthToken()', token);
|
|
58
|
+
this.authToken = token;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
public post<T>(endpoint: string, body: unknown): Observable<T> {
|
|
62
|
+
const url = this.baseUrl + endpoint;
|
|
63
|
+
// console.log('post()', endpoint, url, body);
|
|
64
|
+
return (
|
|
65
|
+
this.errorIfNotAuthenticated() ||
|
|
66
|
+
this.httpClient.post<T>(url, body, {
|
|
67
|
+
headers: this.headers(),
|
|
68
|
+
})
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public put<T>(endpoint: string, body: unknown): Observable<T> {
|
|
73
|
+
return (
|
|
74
|
+
this.errorIfNotAuthenticated() ||
|
|
75
|
+
this.httpClient.put<T>(this.baseUrl + endpoint, body, {
|
|
76
|
+
headers: this.headers(),
|
|
77
|
+
})
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
public get<T>(endpoint: string, params?: HttpParams): Observable<T> {
|
|
82
|
+
return (
|
|
83
|
+
this.errorIfNotAuthenticated() ||
|
|
84
|
+
this.httpClient.get<T>(this.baseUrl + endpoint, {
|
|
85
|
+
headers: this.headers(),
|
|
86
|
+
params,
|
|
87
|
+
})
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
public getAsAnonymous<T>(
|
|
92
|
+
endpoint: string,
|
|
93
|
+
params?: HttpParams,
|
|
94
|
+
): Observable<T> {
|
|
95
|
+
return this.httpClient.get<T>(this.baseUrl + endpoint, {
|
|
96
|
+
params,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
public postAsAnonymous<T>(endpoint: string, body: unknown): Observable<T> {
|
|
101
|
+
const url = this.baseUrl + endpoint;
|
|
102
|
+
// alert('postAsAnonymous(), url=' + url);
|
|
103
|
+
return this.httpClient.post<T>(url, body);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
public delete<T>(
|
|
107
|
+
endpoint: string,
|
|
108
|
+
params?: HttpParams,
|
|
109
|
+
body?: unknown,
|
|
110
|
+
): Observable<T> {
|
|
111
|
+
// console.log('delete()', endpoint, params);
|
|
112
|
+
const url = this.baseUrl + endpoint;
|
|
113
|
+
return (
|
|
114
|
+
this.errorIfNotAuthenticated() ||
|
|
115
|
+
this.httpClient.delete<T>(url, {
|
|
116
|
+
params,
|
|
117
|
+
headers: this.headers(),
|
|
118
|
+
body,
|
|
119
|
+
})
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private errorIfNotAuthenticated(): Observable<never> | undefined {
|
|
124
|
+
const result: Observable<never> | undefined =
|
|
125
|
+
(!this.authToken &&
|
|
126
|
+
throwError(() => userIsNotAuthenticatedNoFirebaseToken)) ||
|
|
127
|
+
undefined;
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private headers(): HttpHeaders {
|
|
132
|
+
let headers = new HttpHeaders();
|
|
133
|
+
if (this.authToken) {
|
|
134
|
+
headers = headers.append('Authorization', 'Bearer ' + this.authToken);
|
|
135
|
+
}
|
|
136
|
+
return headers;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { TestBed } from '@angular/core/testing';
|
|
2
|
+
import { SneatApiModule } from './sneat-api.module';
|
|
3
|
+
|
|
4
|
+
describe('SneatApiModule', () => {
|
|
5
|
+
it('should create module', () => {
|
|
6
|
+
const module = new SneatApiModule();
|
|
7
|
+
expect(module).toBeTruthy();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('should be importable in TestBed', () => {
|
|
11
|
+
TestBed.configureTestingModule({
|
|
12
|
+
imports: [SneatApiModule],
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const module = TestBed.inject(SneatApiModule);
|
|
16
|
+
expect(module).toBeTruthy();
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
import { TestBed, fakeAsync, tick } from '@angular/core/testing';
|
|
2
|
+
import { Injector } from '@angular/core';
|
|
3
|
+
import {
|
|
4
|
+
CollectionReference,
|
|
5
|
+
DocumentReference,
|
|
6
|
+
DocumentSnapshot,
|
|
7
|
+
QuerySnapshot,
|
|
8
|
+
} from '@angular/fire/firestore';
|
|
9
|
+
import { Subject } from 'rxjs';
|
|
10
|
+
import {
|
|
11
|
+
SneatFirestoreService,
|
|
12
|
+
docSnapshotToDto,
|
|
13
|
+
IQueryArgs,
|
|
14
|
+
} from './sneat-firestore.service';
|
|
15
|
+
|
|
16
|
+
// Mock Firebase functions
|
|
17
|
+
const mockDoc = vi.fn();
|
|
18
|
+
const mockGetDoc = vi.fn();
|
|
19
|
+
const mockOnSnapshot = vi.fn();
|
|
20
|
+
const mockQuery = vi.fn();
|
|
21
|
+
const mockWhere = vi.fn();
|
|
22
|
+
const mockLimit = vi.fn();
|
|
23
|
+
|
|
24
|
+
vi.mock('@angular/fire/firestore', () => ({
|
|
25
|
+
doc: (...args: unknown[]) => mockDoc(...args),
|
|
26
|
+
getDoc: (...args: unknown[]) => mockGetDoc(...args),
|
|
27
|
+
onSnapshot: (...args: unknown[]) => mockOnSnapshot(...args),
|
|
28
|
+
query: (...args: unknown[]) => mockQuery(...args),
|
|
29
|
+
where: (...args: unknown[]) => mockWhere(...args),
|
|
30
|
+
limit: (...args: unknown[]) => mockLimit(...args),
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
interface TestBrief {
|
|
34
|
+
id: string;
|
|
35
|
+
name: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface TestDbo extends TestBrief {
|
|
39
|
+
email: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('SneatFirestoreService', () => {
|
|
43
|
+
let service: SneatFirestoreService<TestBrief, TestDbo>;
|
|
44
|
+
let injector: Injector;
|
|
45
|
+
let dto2brief: (id: string, dto: TestDbo) => TestBrief;
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
TestBed.configureTestingModule({
|
|
49
|
+
providers: [],
|
|
50
|
+
});
|
|
51
|
+
injector = TestBed.inject(Injector);
|
|
52
|
+
dto2brief = (id: string, dto: TestDbo) => ({ id, name: dto.name });
|
|
53
|
+
|
|
54
|
+
// Clear all mocks
|
|
55
|
+
mockDoc.mockClear();
|
|
56
|
+
mockGetDoc.mockClear();
|
|
57
|
+
mockOnSnapshot.mockClear();
|
|
58
|
+
mockQuery.mockClear();
|
|
59
|
+
mockWhere.mockClear();
|
|
60
|
+
mockLimit.mockClear();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should be created', () => {
|
|
64
|
+
service = new SneatFirestoreService(injector, dto2brief);
|
|
65
|
+
expect(service).toBeTruthy();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should use default dto2brief if not provided', () => {
|
|
69
|
+
// The constructor has a default value for dto2brief, so it will never throw
|
|
70
|
+
// when undefined is passed - it will just use the default
|
|
71
|
+
service = new SneatFirestoreService(injector);
|
|
72
|
+
expect(service).toBeTruthy();
|
|
73
|
+
|
|
74
|
+
// Test that the default dto2brief works correctly
|
|
75
|
+
const mockSnapshot = {
|
|
76
|
+
id: 'doc1',
|
|
77
|
+
exists: () => true,
|
|
78
|
+
data: () =>
|
|
79
|
+
({ id: 'doc1', name: 'Test', email: 'test1@example.com' }) as TestDbo,
|
|
80
|
+
} as unknown as DocumentSnapshot<TestDbo>;
|
|
81
|
+
|
|
82
|
+
const result = service.docSnapshotToContext(mockSnapshot);
|
|
83
|
+
expect(result.id).toBe('doc1');
|
|
84
|
+
expect(result.dto).toBeDefined();
|
|
85
|
+
expect((result.brief as TestBrief).id).toBe('doc1');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should throw error if dto2brief is explicitly null', () => {
|
|
89
|
+
// Use type assertion to bypass TypeScript and test runtime behavior
|
|
90
|
+
expect(() => {
|
|
91
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
92
|
+
new SneatFirestoreService(injector, null as any);
|
|
93
|
+
}).toThrow('dto2brief is required');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('watchByID', () => {
|
|
97
|
+
it('should watch document by ID', fakeAsync(() => {
|
|
98
|
+
service = new SneatFirestoreService(injector, dto2brief);
|
|
99
|
+
const mockCollection = {
|
|
100
|
+
path: 'test-collection',
|
|
101
|
+
} as CollectionReference<TestDbo>;
|
|
102
|
+
const mockDocRef = {
|
|
103
|
+
id: 'doc1',
|
|
104
|
+
path: 'test-collection/doc1',
|
|
105
|
+
} as DocumentReference<TestDbo>;
|
|
106
|
+
const mockSnapshot = {
|
|
107
|
+
exists: () => true,
|
|
108
|
+
data: () => ({ id: 'doc1', name: 'Test', email: 'test1@example.com' }),
|
|
109
|
+
} as unknown as DocumentSnapshot<TestDbo>;
|
|
110
|
+
|
|
111
|
+
mockDoc.mockReturnValue(mockDocRef);
|
|
112
|
+
mockOnSnapshot.mockImplementation((docRef, next) => {
|
|
113
|
+
// Simulate snapshot
|
|
114
|
+
setTimeout(() => next(mockSnapshot), 0);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const result$ = service.watchByID(mockCollection, 'doc1');
|
|
118
|
+
let result: unknown;
|
|
119
|
+
result$.subscribe((data) => {
|
|
120
|
+
result = data;
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
tick();
|
|
124
|
+
|
|
125
|
+
expect(mockDoc).toHaveBeenCalledWith(mockCollection, 'doc1');
|
|
126
|
+
expect(result).toEqual({
|
|
127
|
+
id: 'doc1',
|
|
128
|
+
dbo: { id: 'doc1', name: 'Test', email: 'test1@example.com' },
|
|
129
|
+
brief: { id: 'doc1', name: 'Test' },
|
|
130
|
+
});
|
|
131
|
+
}));
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('watchByDocRef', () => {
|
|
135
|
+
it('should watch document by reference', fakeAsync(() => {
|
|
136
|
+
service = new SneatFirestoreService(injector, dto2brief);
|
|
137
|
+
const mockDocRef = {
|
|
138
|
+
id: 'doc2',
|
|
139
|
+
path: 'test-collection/doc2',
|
|
140
|
+
} as DocumentReference<TestDbo>;
|
|
141
|
+
const mockSnapshot = {
|
|
142
|
+
exists: () => true,
|
|
143
|
+
data: () => ({ id: 'doc2', name: 'Test2', email: 'test2@example.com' }),
|
|
144
|
+
} as unknown as DocumentSnapshot<TestDbo>;
|
|
145
|
+
|
|
146
|
+
mockOnSnapshot.mockImplementation((docRef, next) => {
|
|
147
|
+
setTimeout(() => next(mockSnapshot), 0);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const result$ = service.watchByDocRef(mockDocRef);
|
|
151
|
+
let result: unknown;
|
|
152
|
+
result$.subscribe((data) => {
|
|
153
|
+
result = data;
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
tick();
|
|
157
|
+
|
|
158
|
+
expect(mockOnSnapshot).toHaveBeenCalled();
|
|
159
|
+
expect(result).toEqual({
|
|
160
|
+
id: 'doc2',
|
|
161
|
+
dbo: { id: 'doc2', name: 'Test2', email: 'test2@example.com' },
|
|
162
|
+
brief: { id: 'doc2', name: 'Test2' },
|
|
163
|
+
});
|
|
164
|
+
}));
|
|
165
|
+
|
|
166
|
+
it('should handle errors', fakeAsync(() => {
|
|
167
|
+
service = new SneatFirestoreService(injector, dto2brief);
|
|
168
|
+
const mockDocRef = {
|
|
169
|
+
id: 'doc3',
|
|
170
|
+
path: 'test-collection/doc3',
|
|
171
|
+
} as DocumentReference<TestDbo>;
|
|
172
|
+
const testError = new Error('Firestore error');
|
|
173
|
+
|
|
174
|
+
mockOnSnapshot.mockImplementation((docRef, next, error) => {
|
|
175
|
+
setTimeout(() => error(testError), 0);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const result$ = service.watchByDocRef(mockDocRef);
|
|
179
|
+
let caughtError: unknown;
|
|
180
|
+
result$.subscribe({
|
|
181
|
+
error: (err) => {
|
|
182
|
+
caughtError = err;
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
tick();
|
|
187
|
+
|
|
188
|
+
expect(caughtError).toBe(testError);
|
|
189
|
+
}));
|
|
190
|
+
|
|
191
|
+
it('should handle complete', fakeAsync(() => {
|
|
192
|
+
service = new SneatFirestoreService(injector, dto2brief);
|
|
193
|
+
const mockDocRef = {
|
|
194
|
+
id: 'doc5',
|
|
195
|
+
path: 'test-collection/doc5',
|
|
196
|
+
} as DocumentReference<TestDbo>;
|
|
197
|
+
|
|
198
|
+
mockOnSnapshot.mockImplementation((docRef, next, error, complete) => {
|
|
199
|
+
setTimeout(() => complete(), 0);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const result$ = service.watchByDocRef(mockDocRef);
|
|
203
|
+
let completed = false;
|
|
204
|
+
result$.subscribe({
|
|
205
|
+
complete: () => {
|
|
206
|
+
completed = true;
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
tick();
|
|
211
|
+
|
|
212
|
+
expect(completed).toBe(true);
|
|
213
|
+
}));
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe('getByDocRef', () => {
|
|
217
|
+
it('should get document by reference', fakeAsync(() => {
|
|
218
|
+
service = new SneatFirestoreService(injector, dto2brief);
|
|
219
|
+
const mockDocRef = {
|
|
220
|
+
id: 'doc4',
|
|
221
|
+
path: 'test-collection/doc4',
|
|
222
|
+
} as DocumentReference<TestDbo>;
|
|
223
|
+
const mockSnapshot = {
|
|
224
|
+
exists: () => true,
|
|
225
|
+
data: () => ({ id: 'doc4', name: 'Test4', email: 'test4@example.com' }),
|
|
226
|
+
} as unknown as DocumentSnapshot<TestDbo>;
|
|
227
|
+
|
|
228
|
+
mockGetDoc.mockResolvedValue(mockSnapshot);
|
|
229
|
+
|
|
230
|
+
const result$ = service.getByDocRef(mockDocRef);
|
|
231
|
+
let result: unknown;
|
|
232
|
+
result$.subscribe((data) => {
|
|
233
|
+
result = data;
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
tick();
|
|
237
|
+
|
|
238
|
+
expect(mockGetDoc).toHaveBeenCalledWith(mockDocRef);
|
|
239
|
+
expect(result).toEqual({
|
|
240
|
+
id: 'doc4',
|
|
241
|
+
dbo: { id: 'doc4', name: 'Test4', email: 'test4@example.com' },
|
|
242
|
+
brief: { id: 'doc4', name: 'Test4' },
|
|
243
|
+
});
|
|
244
|
+
}));
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
describe('watchSnapshotsByFilter', () => {
|
|
248
|
+
it('should watch documents by filter', () => {
|
|
249
|
+
service = new SneatFirestoreService(injector, dto2brief);
|
|
250
|
+
const mockCollection = {
|
|
251
|
+
path: 'test-collection',
|
|
252
|
+
} as CollectionReference<TestDbo>;
|
|
253
|
+
const queryArgs: IQueryArgs = {
|
|
254
|
+
filter: [{ field: 'name', operator: '==', value: 'Test' }],
|
|
255
|
+
};
|
|
256
|
+
const mockQueryObj = {};
|
|
257
|
+
|
|
258
|
+
mockWhere.mockReturnValue({});
|
|
259
|
+
mockQuery.mockReturnValue(mockQueryObj);
|
|
260
|
+
mockOnSnapshot.mockImplementation(() => {
|
|
261
|
+
// Just return to simulate subscription
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const result$ = service.watchSnapshotsByFilter(mockCollection, queryArgs);
|
|
265
|
+
|
|
266
|
+
expect(mockQuery).toHaveBeenCalled();
|
|
267
|
+
expect(mockWhere).toHaveBeenCalledWith('name', '==', 'Test');
|
|
268
|
+
expect(result$).toBeTruthy();
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('should use array-contains for fields ending with IDs', () => {
|
|
272
|
+
service = new SneatFirestoreService(injector, dto2brief);
|
|
273
|
+
const mockCollection = {
|
|
274
|
+
path: 'test-collection',
|
|
275
|
+
} as CollectionReference<TestDbo>;
|
|
276
|
+
const queryArgs: IQueryArgs = {
|
|
277
|
+
filter: [{ field: 'userIDs', operator: '==', value: 'user1' }],
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
mockWhere.mockReturnValue({});
|
|
281
|
+
mockQuery.mockReturnValue({});
|
|
282
|
+
mockOnSnapshot.mockImplementation(() => {
|
|
283
|
+
// Just return
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
service.watchSnapshotsByFilter(mockCollection, queryArgs);
|
|
287
|
+
|
|
288
|
+
expect(mockWhere).toHaveBeenCalledWith(
|
|
289
|
+
'userIDs',
|
|
290
|
+
'array-contains',
|
|
291
|
+
'user1',
|
|
292
|
+
);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('should apply limit when specified', () => {
|
|
296
|
+
service = new SneatFirestoreService(injector, dto2brief);
|
|
297
|
+
const mockCollection = {
|
|
298
|
+
path: 'test-collection',
|
|
299
|
+
} as CollectionReference<TestDbo>;
|
|
300
|
+
const queryArgs: IQueryArgs = {
|
|
301
|
+
limit: 10,
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
mockLimit.mockReturnValue({});
|
|
305
|
+
mockQuery.mockReturnValue({});
|
|
306
|
+
mockOnSnapshot.mockImplementation(() => {
|
|
307
|
+
// Just return
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
service.watchSnapshotsByFilter(mockCollection, queryArgs);
|
|
311
|
+
|
|
312
|
+
expect(mockLimit).toHaveBeenCalledWith(10);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
describe('watchByFilter', () => {
|
|
317
|
+
it('should watch and transform documents by filter', fakeAsync(() => {
|
|
318
|
+
service = new SneatFirestoreService(injector, dto2brief);
|
|
319
|
+
const mockCollection = {
|
|
320
|
+
path: 'test-collection',
|
|
321
|
+
} as CollectionReference<TestDbo>;
|
|
322
|
+
const mockSnapshot1 = {
|
|
323
|
+
id: 'doc1',
|
|
324
|
+
exists: () => true,
|
|
325
|
+
data: () => ({ id: 'doc1', name: 'Test1', email: 'test1@example.com' }),
|
|
326
|
+
} as unknown as DocumentSnapshot<TestDbo>;
|
|
327
|
+
const mockQuerySnapshot = {
|
|
328
|
+
docs: [mockSnapshot1],
|
|
329
|
+
} as QuerySnapshot<TestDbo>;
|
|
330
|
+
|
|
331
|
+
mockQuery.mockReturnValue({});
|
|
332
|
+
mockOnSnapshot.mockImplementation(
|
|
333
|
+
(q, subj: Subject<QuerySnapshot<TestDbo>>) => {
|
|
334
|
+
setTimeout(() => subj.next(mockQuerySnapshot), 0);
|
|
335
|
+
},
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
const result$ = service.watchByFilter(mockCollection);
|
|
339
|
+
let result: unknown;
|
|
340
|
+
result$.subscribe((data) => {
|
|
341
|
+
result = data;
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
tick();
|
|
345
|
+
|
|
346
|
+
expect(result).toEqual([
|
|
347
|
+
{
|
|
348
|
+
id: 'doc1',
|
|
349
|
+
dto: { id: 'doc1', name: 'Test1', email: 'test1@example.com' },
|
|
350
|
+
brief: { id: 'doc1', name: 'Test1' },
|
|
351
|
+
},
|
|
352
|
+
]);
|
|
353
|
+
}));
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
describe('docSnapshotsToContext', () => {
|
|
357
|
+
it('should transform array of snapshots to contexts', () => {
|
|
358
|
+
service = new SneatFirestoreService(injector, dto2brief);
|
|
359
|
+
const mockSnapshot1 = {
|
|
360
|
+
id: 'doc1',
|
|
361
|
+
exists: () => true,
|
|
362
|
+
data: () => ({ id: 'doc1', name: 'Test1', email: 'test1@example.com' }),
|
|
363
|
+
} as unknown as DocumentSnapshot<TestDbo>;
|
|
364
|
+
const mockSnapshot2 = {
|
|
365
|
+
id: 'doc2',
|
|
366
|
+
exists: () => true,
|
|
367
|
+
data: () => ({ id: 'doc2', name: 'Test2', email: 'test2@example.com' }),
|
|
368
|
+
} as unknown as DocumentSnapshot<TestDbo>;
|
|
369
|
+
|
|
370
|
+
const result = service.docSnapshotsToContext([
|
|
371
|
+
mockSnapshot1,
|
|
372
|
+
mockSnapshot2,
|
|
373
|
+
]);
|
|
374
|
+
|
|
375
|
+
expect(result).toHaveLength(2);
|
|
376
|
+
expect(result[0]).toEqual({
|
|
377
|
+
id: 'doc1',
|
|
378
|
+
dto: { id: 'doc1', name: 'Test1', email: 'test1@example.com' },
|
|
379
|
+
brief: { id: 'doc1', name: 'Test1' },
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
describe('docSnapshotToContext', () => {
|
|
385
|
+
it('should transform snapshot to context', () => {
|
|
386
|
+
service = new SneatFirestoreService(injector, dto2brief);
|
|
387
|
+
const mockSnapshot = {
|
|
388
|
+
id: 'doc1',
|
|
389
|
+
exists: () => true,
|
|
390
|
+
data: () => ({ id: 'doc1', name: 'Test1', email: 'test1@example.com' }),
|
|
391
|
+
} as unknown as DocumentSnapshot<TestDbo>;
|
|
392
|
+
|
|
393
|
+
const result = service.docSnapshotToContext(mockSnapshot);
|
|
394
|
+
|
|
395
|
+
expect(result).toEqual({
|
|
396
|
+
id: 'doc1',
|
|
397
|
+
dto: { id: 'doc1', name: 'Test1', email: 'test1@example.com' },
|
|
398
|
+
brief: { id: 'doc1', name: 'Test1' },
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('should handle snapshot with no data', () => {
|
|
403
|
+
service = new SneatFirestoreService(injector, dto2brief);
|
|
404
|
+
const mockSnapshot = {
|
|
405
|
+
id: 'doc1',
|
|
406
|
+
exists: () => true,
|
|
407
|
+
data: () => undefined,
|
|
408
|
+
} as unknown as DocumentSnapshot<TestDbo>;
|
|
409
|
+
|
|
410
|
+
const result = service.docSnapshotToContext(mockSnapshot);
|
|
411
|
+
|
|
412
|
+
expect(result).toEqual({
|
|
413
|
+
id: 'doc1',
|
|
414
|
+
dto: undefined,
|
|
415
|
+
brief: undefined,
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
describe('docSnapshotToDto', () => {
|
|
421
|
+
it('should convert existing snapshot to dto', () => {
|
|
422
|
+
const mockSnapshot = {
|
|
423
|
+
exists: true,
|
|
424
|
+
data: () => ({ id: 'doc1', name: 'Test1', email: 'test1@example.com' }),
|
|
425
|
+
} as unknown as DocumentSnapshot<TestDbo>;
|
|
426
|
+
|
|
427
|
+
const result = docSnapshotToDto('doc1', dto2brief, mockSnapshot);
|
|
428
|
+
|
|
429
|
+
expect(result).toEqual({
|
|
430
|
+
id: 'doc1',
|
|
431
|
+
dbo: { id: 'doc1', name: 'Test1', email: 'test1@example.com' },
|
|
432
|
+
brief: { id: 'doc1', name: 'Test1' },
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it('should handle non-existing snapshot', () => {
|
|
437
|
+
const mockSnapshot = {
|
|
438
|
+
exists: false,
|
|
439
|
+
} as unknown as DocumentSnapshot<TestDbo>;
|
|
440
|
+
|
|
441
|
+
const result = docSnapshotToDto('doc1', dto2brief, mockSnapshot);
|
|
442
|
+
|
|
443
|
+
expect(result).toEqual({
|
|
444
|
+
id: 'doc1',
|
|
445
|
+
brief: null,
|
|
446
|
+
dbo: null,
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it('should handle snapshot with undefined data', () => {
|
|
451
|
+
const mockSnapshot = {
|
|
452
|
+
exists: true,
|
|
453
|
+
data: () => undefined,
|
|
454
|
+
} as unknown as DocumentSnapshot<TestDbo>;
|
|
455
|
+
|
|
456
|
+
const result = docSnapshotToDto('doc1', dto2brief, mockSnapshot);
|
|
457
|
+
|
|
458
|
+
expect(result).toEqual({
|
|
459
|
+
id: 'doc1',
|
|
460
|
+
dbo: undefined,
|
|
461
|
+
brief: undefined,
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
});
|