http-request-manager 18.15.32 → 18.15.33
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.
|
@@ -4,10 +4,11 @@ import { ComponentStore } from '@ngrx/component-store';
|
|
|
4
4
|
import { map, catchError, filter, take, tap, finalize, takeWhile, retry, startWith, mergeMap, takeUntil, concatMap, toArray, withLatestFrom, switchMap, delay, scan, distinctUntilChanged } from 'rxjs/operators';
|
|
5
5
|
import { HttpClient, HttpHeaders, HttpEventType, HttpHeaderResponse, HttpErrorResponse, HTTP_INTERCEPTORS } from '@angular/common/http';
|
|
6
6
|
import * as CryptoJS from 'crypto-js';
|
|
7
|
-
import { from, BehaviorSubject, EMPTY, timer, throwError, defer, interval, Subject, of, merge, Subscription, take as take$1, catchError as catchError$1, map as map$1, tap as tap$1, switchMap as switchMap$1, startWith as startWith$1, distinctUntilChanged as distinctUntilChanged$1, combineLatest, filter as filter$1, takeUntil as takeUntil$1, ReplaySubject } from 'rxjs';
|
|
7
|
+
import { from, BehaviorSubject, EMPTY, timer, throwError, defer, interval, Subject, of, merge, Subscription, take as take$1, firstValueFrom, catchError as catchError$1, map as map$1, tap as tap$1, switchMap as switchMap$1, startWith as startWith$1, distinctUntilChanged as distinctUntilChanged$1, combineLatest, filter as filter$1, takeUntil as takeUntil$1, ReplaySubject } from 'rxjs';
|
|
8
8
|
import { toObservable, takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
9
9
|
import { ToastMessageDisplayService, ToastDisplay, ToastColors, ToastMessageDisplayModule } from 'toast-message-display';
|
|
10
10
|
import Dexie from 'dexie';
|
|
11
|
+
import { Parser } from 'node-sql-parser/build/mysql';
|
|
11
12
|
import * as i1 from '@ngx-translate/core';
|
|
12
13
|
import { TranslateModule } from '@ngx-translate/core';
|
|
13
14
|
import * as i1$1 from '@angular/common';
|
|
@@ -5287,7 +5288,7 @@ class LocalStorageManagerService extends ComponentStore {
|
|
|
5287
5288
|
? [...data.localStores].reverse().find(item => item.id === foundStore.id)
|
|
5288
5289
|
: [...data.sessionStores].reverse().find(item => item.id === foundStore.id);
|
|
5289
5290
|
if (!found) {
|
|
5290
|
-
console.warn('[CacheDebug] store$: settings entry found but no data entry in localStores/sessionStores', { store, foundStoreId: foundStore.id, storageType: foundStore.options?.storage })
|
|
5291
|
+
// console.warn('[CacheDebug] store$: settings entry found but no data entry in localStores/sessionStores', { store, foundStoreId: foundStore.id, storageType: foundStore.options?.storage })
|
|
5291
5292
|
this.deleteStore({ name: store });
|
|
5292
5293
|
return;
|
|
5293
5294
|
}
|
|
@@ -5344,7 +5345,7 @@ class LocalStorageManagerService extends ComponentStore {
|
|
|
5344
5345
|
const hasStore = this.hasGlobalStorage(type, store.id);
|
|
5345
5346
|
store.name = this.validStoreName(store.name);
|
|
5346
5347
|
if (!hasStore) {
|
|
5347
|
-
console.warn(`No such Store: ${store.name}`)
|
|
5348
|
+
// console.warn(`No such Store: ${store.name}`)
|
|
5348
5349
|
return state;
|
|
5349
5350
|
}
|
|
5350
5351
|
else {
|
|
@@ -5398,23 +5399,14 @@ class LocalStorageManagerService extends ComponentStore {
|
|
|
5398
5399
|
store.name = this.validStoreName(store.name);
|
|
5399
5400
|
const settings = [...state.settings].reverse().find(item => item.name === store.name);
|
|
5400
5401
|
if (settings) {
|
|
5401
|
-
const
|
|
5402
|
-
const
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
const sessionData = (dataStr && settings.options?.storage === StorageType.SESSION) ? [{ data: dataStr, id: settings.id }] : [];
|
|
5410
|
-
state.localStores = state.localStores.filter(item => item.id !== settings.id);
|
|
5411
|
-
state.sessionStores = state.sessionStores.filter(item => item.id !== settings.id);
|
|
5412
|
-
return {
|
|
5413
|
-
...state,
|
|
5414
|
-
localStores: [...state.localStores, ...localData],
|
|
5415
|
-
sessionStores: [...state.sessionStores, ...sessionData],
|
|
5416
|
-
};
|
|
5417
|
-
}
|
|
5402
|
+
const dataStr = (settings.options?.encrypted) ? this.encryption.encrypt(store.data, this.app.appID) : store.data;
|
|
5403
|
+
const localData = (dataStr && settings.options?.storage === StorageType.GLOBAL) ? [{ data: dataStr, id: settings.id }] : [];
|
|
5404
|
+
const sessionData = (dataStr && settings.options?.storage === StorageType.SESSION) ? [{ data: dataStr, id: settings.id }] : [];
|
|
5405
|
+
return {
|
|
5406
|
+
...state,
|
|
5407
|
+
localStores: [...state.localStores.filter(item => item.id !== settings.id), ...localData],
|
|
5408
|
+
sessionStores: [...state.sessionStores.filter(item => item.id !== settings.id), ...sessionData],
|
|
5409
|
+
};
|
|
5418
5410
|
}
|
|
5419
5411
|
return state;
|
|
5420
5412
|
});
|
|
@@ -5434,7 +5426,7 @@ class LocalStorageManagerService extends ComponentStore {
|
|
|
5434
5426
|
};
|
|
5435
5427
|
}
|
|
5436
5428
|
else {
|
|
5437
|
-
console.warn(`No such Store: ${store.name}`)
|
|
5429
|
+
// console.warn(`No such Store: ${store.name}`)
|
|
5438
5430
|
return state;
|
|
5439
5431
|
}
|
|
5440
5432
|
});
|
|
@@ -5593,9 +5585,15 @@ class LocalStorageManagerService extends ComponentStore {
|
|
|
5593
5585
|
*/
|
|
5594
5586
|
clearAllStoredData() {
|
|
5595
5587
|
try {
|
|
5596
|
-
localStorage.
|
|
5597
|
-
localStorage
|
|
5598
|
-
|
|
5588
|
+
// Use resetStore() instead of directly removing raw localStorage keys.
|
|
5589
|
+
// clearAllStoredData previously only removed the raw localStorage entries, leaving
|
|
5590
|
+
// the ComponentStore's in-memory BehaviorSubject untouched. Any subsequent call to
|
|
5591
|
+
// store$(name) would read from the in-memory state and return the old (stale) data,
|
|
5592
|
+
// because store$ is a ComponentStore selector — it reads from in-memory state, not
|
|
5593
|
+
// from raw localStorage. This caused stale requestCache / queryParams to persist
|
|
5594
|
+
// after a deleteDatabase() call, blocking all subsequent API requests.
|
|
5595
|
+
// resetStore() correctly updates both the in-memory state AND persists to localStorage.
|
|
5596
|
+
this.resetStore();
|
|
5599
5597
|
console.log('Cleared all stored data');
|
|
5600
5598
|
}
|
|
5601
5599
|
catch (error) {
|
|
@@ -6493,8 +6491,8 @@ class DatabaseManagerService extends DbService {
|
|
|
6493
6491
|
}
|
|
6494
6492
|
}
|
|
6495
6493
|
clearDatabase() {
|
|
6496
|
-
|
|
6497
|
-
console.error('clearDatabase: ❌ Error
|
|
6494
|
+
this.deleteAndReinitialize().pipe(tap(() => this.localStorageManager.clearAllStoredData()), catchError((error) => {
|
|
6495
|
+
console.error('clearDatabase: ❌ Error clearing database:', error);
|
|
6498
6496
|
return of(false);
|
|
6499
6497
|
})).subscribe();
|
|
6500
6498
|
}
|
|
@@ -6526,347 +6524,7 @@ class ChannelMessage {
|
|
|
6526
6524
|
}
|
|
6527
6525
|
}
|
|
6528
6526
|
|
|
6529
|
-
|
|
6530
|
-
const TRACKER_SESSION_INIT_KEY = 'query_params_tracker_initialized';
|
|
6531
|
-
const DEFAULT_TRACKER_OPTIONS = SettingOptions.adapt({
|
|
6532
|
-
storage: StorageType.GLOBAL,
|
|
6533
|
-
encrypted: false,
|
|
6534
|
-
});
|
|
6535
|
-
class QueryParamsTrackerService {
|
|
6536
|
-
constructor() {
|
|
6537
|
-
this.state = { paths: {} };
|
|
6538
|
-
this.stateRestored = false;
|
|
6539
|
-
this.localStorageManager = inject(LocalStorageManagerService);
|
|
6540
|
-
this.ready$ = new BehaviorSubject(false);
|
|
6541
|
-
}
|
|
6542
|
-
clearTracking(resetSessionInit = false) {
|
|
6543
|
-
this.state = { paths: {} };
|
|
6544
|
-
this.localStorageManager.deleteStore({ name: TRACKER_STORE_NAME });
|
|
6545
|
-
if (resetSessionInit && this.hasSessionStorage()) {
|
|
6546
|
-
sessionStorage.removeItem(TRACKER_SESSION_INIT_KEY);
|
|
6547
|
-
}
|
|
6548
|
-
}
|
|
6549
|
-
clearTrackingForPath(pathKey) {
|
|
6550
|
-
this.ensureStateRestored();
|
|
6551
|
-
if (this.state.paths[pathKey]) {
|
|
6552
|
-
delete this.state.paths[pathKey];
|
|
6553
|
-
this.persistState();
|
|
6554
|
-
}
|
|
6555
|
-
}
|
|
6556
|
-
checkRequestOptions(requestOptions = [], options = {}) {
|
|
6557
|
-
this.ensureStateRestored();
|
|
6558
|
-
if (!this.ready$.value) {
|
|
6559
|
-
return true;
|
|
6560
|
-
}
|
|
6561
|
-
const normalized = this.normalizeRequestOptions(requestOptions);
|
|
6562
|
-
if (!normalized.pathKey)
|
|
6563
|
-
return false;
|
|
6564
|
-
this.cleanupExpiredEntries();
|
|
6565
|
-
if (!normalized.hasQuery) {
|
|
6566
|
-
const pathState = this.ensurePathState(normalized.pathKey);
|
|
6567
|
-
const pathSeenBefore = Object.keys(pathState.consumedValuesByKey).length > 0;
|
|
6568
|
-
if (!pathSeenBefore) {
|
|
6569
|
-
this.persistState();
|
|
6570
|
-
}
|
|
6571
|
-
return !pathSeenBefore;
|
|
6572
|
-
}
|
|
6573
|
-
if (options.mode === 'exact') {
|
|
6574
|
-
return this.checkExact(normalized, options);
|
|
6575
|
-
}
|
|
6576
|
-
return this.checkVariation(normalized, options);
|
|
6577
|
-
}
|
|
6578
|
-
matchesPath(requestOptions = [], expectedPathOptions = []) {
|
|
6579
|
-
this.ensureStateRestored();
|
|
6580
|
-
const requestPath = this.normalizeRequestOptions(requestOptions).pathKey;
|
|
6581
|
-
const expectedPath = this.normalizeRequestOptions(expectedPathOptions).pathKey;
|
|
6582
|
-
return Boolean(requestPath) && requestPath === expectedPath;
|
|
6583
|
-
}
|
|
6584
|
-
checkExact(normalized, options) {
|
|
6585
|
-
const pathState = this.ensurePathState(normalized.pathKey);
|
|
6586
|
-
const filteredQuery = this.filterQueryByWatchParams(normalized.query, options.watchParams);
|
|
6587
|
-
if (Object.keys(filteredQuery).length === 0) {
|
|
6588
|
-
return false;
|
|
6589
|
-
}
|
|
6590
|
-
const hash = this.stringifyQuery(filteredQuery);
|
|
6591
|
-
const consumed = pathState.consumedValuesByKey['__exact__'] || [];
|
|
6592
|
-
if (!consumed.includes(hash)) {
|
|
6593
|
-
pathState.consumedValuesByKey['__exact__'] = [...consumed, hash];
|
|
6594
|
-
this.persistState();
|
|
6595
|
-
return true;
|
|
6596
|
-
}
|
|
6597
|
-
return false;
|
|
6598
|
-
}
|
|
6599
|
-
checkVariation(normalized, options) {
|
|
6600
|
-
const pathState = this.ensurePathState(normalized.pathKey);
|
|
6601
|
-
if (Array.isArray(options.watchParams) && options.watchParams.length === 0) {
|
|
6602
|
-
const pathSeenBefore = Object.keys(pathState.consumedValuesByKey).length > 0;
|
|
6603
|
-
if (!pathSeenBefore) {
|
|
6604
|
-
pathState.watchExpiresAt = this.buildExpiryEpoch(typeof options.watchExpiresAt !== 'undefined' ? options.watchExpiresAt : options.watchParamsExpire);
|
|
6605
|
-
this.persistState();
|
|
6606
|
-
}
|
|
6607
|
-
return !pathSeenBefore;
|
|
6608
|
-
}
|
|
6609
|
-
const filteredQuery = this.filterQueryByWatchParams(normalized.query, options.watchParams);
|
|
6610
|
-
const keys = Object.keys(filteredQuery);
|
|
6611
|
-
if (keys.length === 0) {
|
|
6612
|
-
return false;
|
|
6613
|
-
}
|
|
6614
|
-
this.resetPathStateIfExpired(pathState);
|
|
6615
|
-
let accepted = false;
|
|
6616
|
-
keys.forEach((key) => {
|
|
6617
|
-
const currentValue = filteredQuery[key];
|
|
6618
|
-
const consumed = pathState.consumedValuesByKey[key] || [];
|
|
6619
|
-
if (!consumed.includes(currentValue)) {
|
|
6620
|
-
pathState.consumedValuesByKey[key] = [...consumed, currentValue];
|
|
6621
|
-
accepted = true;
|
|
6622
|
-
}
|
|
6623
|
-
});
|
|
6624
|
-
if (accepted) {
|
|
6625
|
-
pathState.watchExpiresAt = this.buildExpiryEpoch(typeof options.watchExpiresAt !== 'undefined' ? options.watchExpiresAt : options.watchParamsExpire);
|
|
6626
|
-
this.persistState();
|
|
6627
|
-
}
|
|
6628
|
-
return accepted;
|
|
6629
|
-
}
|
|
6630
|
-
filterQueryByWatchParams(query, watchParams) {
|
|
6631
|
-
if (!Array.isArray(watchParams)) {
|
|
6632
|
-
return query;
|
|
6633
|
-
}
|
|
6634
|
-
if (watchParams.length === 0) {
|
|
6635
|
-
return {};
|
|
6636
|
-
}
|
|
6637
|
-
const normalizedWatchParams = watchParams.map((p) => this.normalizeParamKey(p));
|
|
6638
|
-
return Object.keys(query).reduce((acc, key) => {
|
|
6639
|
-
if (normalizedWatchParams.includes(key)) {
|
|
6640
|
-
acc[key] = query[key];
|
|
6641
|
-
}
|
|
6642
|
-
return acc;
|
|
6643
|
-
}, {});
|
|
6644
|
-
}
|
|
6645
|
-
normalizeRequestOptions(requestOptions) {
|
|
6646
|
-
const params = Array.isArray(requestOptions) ? requestOptions : [];
|
|
6647
|
-
const pathSegments = [];
|
|
6648
|
-
const queryObjects = [];
|
|
6649
|
-
params.forEach((item) => {
|
|
6650
|
-
if (this.isPlainObject(item)) {
|
|
6651
|
-
queryObjects.push(item);
|
|
6652
|
-
}
|
|
6653
|
-
else if (typeof item !== 'undefined' && item !== null) {
|
|
6654
|
-
const parsed = this.parsePathSegment(String(item));
|
|
6655
|
-
if (parsed.pathSegment) {
|
|
6656
|
-
pathSegments.push(parsed.pathSegment);
|
|
6657
|
-
}
|
|
6658
|
-
if (Object.keys(parsed.queryObject).length > 0) {
|
|
6659
|
-
queryObjects.push(parsed.queryObject);
|
|
6660
|
-
}
|
|
6661
|
-
}
|
|
6662
|
-
});
|
|
6663
|
-
const query = queryObjects.reduce((acc, current) => {
|
|
6664
|
-
Object.keys(current).forEach((key) => {
|
|
6665
|
-
const normalizedKey = this.normalizeParamKey(key);
|
|
6666
|
-
const value = current[key];
|
|
6667
|
-
if (!normalizedKey || typeof value === 'undefined' || value === null || value === '') {
|
|
6668
|
-
return;
|
|
6669
|
-
}
|
|
6670
|
-
acc[normalizedKey] = this.normalizeParamValue(value);
|
|
6671
|
-
});
|
|
6672
|
-
return acc;
|
|
6673
|
-
}, {});
|
|
6674
|
-
return {
|
|
6675
|
-
pathKey: this.normalizePath(pathSegments),
|
|
6676
|
-
query,
|
|
6677
|
-
hasQuery: Object.keys(query).length > 0,
|
|
6678
|
-
};
|
|
6679
|
-
}
|
|
6680
|
-
parsePathSegment(rawSegment) {
|
|
6681
|
-
const [pathPart, ...queryParts] = String(rawSegment).split('?');
|
|
6682
|
-
const queryString = queryParts.join('?');
|
|
6683
|
-
const queryObject = {};
|
|
6684
|
-
if (queryString) {
|
|
6685
|
-
queryString.split('&').forEach((pair) => {
|
|
6686
|
-
if (!pair)
|
|
6687
|
-
return;
|
|
6688
|
-
const [rawKey, ...rawValueParts] = pair.split('=');
|
|
6689
|
-
const key = this.safeDecode(rawKey).trim();
|
|
6690
|
-
const value = this.safeDecode(rawValueParts.join('=')).trim();
|
|
6691
|
-
if (!key)
|
|
6692
|
-
return;
|
|
6693
|
-
queryObject[key] = value;
|
|
6694
|
-
});
|
|
6695
|
-
}
|
|
6696
|
-
return {
|
|
6697
|
-
pathSegment: pathPart,
|
|
6698
|
-
queryObject,
|
|
6699
|
-
};
|
|
6700
|
-
}
|
|
6701
|
-
safeDecode(value) {
|
|
6702
|
-
if (typeof value === 'undefined') {
|
|
6703
|
-
return '';
|
|
6704
|
-
}
|
|
6705
|
-
try {
|
|
6706
|
-
return decodeURIComponent(value);
|
|
6707
|
-
}
|
|
6708
|
-
catch {
|
|
6709
|
-
return String(value);
|
|
6710
|
-
}
|
|
6711
|
-
}
|
|
6712
|
-
normalizePath(pathSegments) {
|
|
6713
|
-
return pathSegments
|
|
6714
|
-
.map((segment) => String(segment).trim())
|
|
6715
|
-
.filter((segment) => segment.length > 0)
|
|
6716
|
-
.join('/')
|
|
6717
|
-
.replace(/([^:]\/+)\/+/g, '$1')
|
|
6718
|
-
.replace(/^\//, '')
|
|
6719
|
-
.replace(/\/$/, '');
|
|
6720
|
-
}
|
|
6721
|
-
normalizeParamKey(key) {
|
|
6722
|
-
return String(key).trim().toLowerCase();
|
|
6723
|
-
}
|
|
6724
|
-
normalizeParamValue(value) {
|
|
6725
|
-
if (Array.isArray(value)) {
|
|
6726
|
-
return value.map((item) => this.normalizeParamValue(item)).join(',');
|
|
6727
|
-
}
|
|
6728
|
-
if (this.isPlainObject(value)) {
|
|
6729
|
-
return this.stringifyQuery(Object.keys(value).reduce((acc, key) => {
|
|
6730
|
-
acc[this.normalizeParamKey(key)] = this.normalizeParamValue(value[key]);
|
|
6731
|
-
return acc;
|
|
6732
|
-
}, {}));
|
|
6733
|
-
}
|
|
6734
|
-
return String(value).trim().toLowerCase();
|
|
6735
|
-
}
|
|
6736
|
-
buildExpiryEpoch(expireIn) {
|
|
6737
|
-
if (typeof expireIn === 'undefined' || expireIn === null || expireIn === '') {
|
|
6738
|
-
return undefined;
|
|
6739
|
-
}
|
|
6740
|
-
if (typeof expireIn === 'number' && Number.isFinite(expireIn) && expireIn > 0) {
|
|
6741
|
-
return Math.floor(Date.now() / 1000) + Math.floor(expireIn);
|
|
6742
|
-
}
|
|
6743
|
-
const raw = String(expireIn).trim().toLowerCase().replace(/\s+/g, '');
|
|
6744
|
-
const parsed = raw.match(/^(\d+)(y|w|d|hr|h|mn|min|m|s)$/);
|
|
6745
|
-
if (!parsed) {
|
|
6746
|
-
return undefined;
|
|
6747
|
-
}
|
|
6748
|
-
const value = Number(parsed[1]);
|
|
6749
|
-
const unit = parsed[2];
|
|
6750
|
-
const toSeconds = {
|
|
6751
|
-
y: 31556926,
|
|
6752
|
-
w: 604800,
|
|
6753
|
-
d: 86400,
|
|
6754
|
-
hr: 3600,
|
|
6755
|
-
h: 3600,
|
|
6756
|
-
mn: 60,
|
|
6757
|
-
min: 60,
|
|
6758
|
-
m: 60,
|
|
6759
|
-
s: 1,
|
|
6760
|
-
};
|
|
6761
|
-
const seconds = toSeconds[unit];
|
|
6762
|
-
if (!seconds) {
|
|
6763
|
-
return undefined;
|
|
6764
|
-
}
|
|
6765
|
-
return Math.floor(Date.now() / 1000) + value * seconds;
|
|
6766
|
-
}
|
|
6767
|
-
resetPathStateIfExpired(pathState) {
|
|
6768
|
-
const now = Math.floor(Date.now() / 1000);
|
|
6769
|
-
if (pathState.watchExpiresAt && pathState.watchExpiresAt <= now) {
|
|
6770
|
-
pathState.consumedValuesByKey = {};
|
|
6771
|
-
pathState.watchExpiresAt = undefined;
|
|
6772
|
-
}
|
|
6773
|
-
}
|
|
6774
|
-
cleanupExpiredEntries() {
|
|
6775
|
-
const now = Math.floor(Date.now() / 1000);
|
|
6776
|
-
Object.keys(this.state.paths).forEach((pathKey) => {
|
|
6777
|
-
const pathState = this.state.paths[pathKey];
|
|
6778
|
-
if (pathState.watchExpiresAt && pathState.watchExpiresAt <= now) {
|
|
6779
|
-
pathState.consumedValuesByKey = {};
|
|
6780
|
-
pathState.watchExpiresAt = undefined;
|
|
6781
|
-
}
|
|
6782
|
-
const hasTrackedValues = Object.keys(pathState.consumedValuesByKey).some((key) => (pathState.consumedValuesByKey[key] || []).length > 0);
|
|
6783
|
-
if (!hasTrackedValues) {
|
|
6784
|
-
delete this.state.paths[pathKey];
|
|
6785
|
-
}
|
|
6786
|
-
});
|
|
6787
|
-
}
|
|
6788
|
-
ensurePathState(pathKey) {
|
|
6789
|
-
if (!this.state.paths[pathKey]) {
|
|
6790
|
-
this.state.paths[pathKey] = {
|
|
6791
|
-
consumedValuesByKey: {},
|
|
6792
|
-
};
|
|
6793
|
-
}
|
|
6794
|
-
return this.state.paths[pathKey];
|
|
6795
|
-
}
|
|
6796
|
-
ensureStateRestored() {
|
|
6797
|
-
if (this.stateRestored) {
|
|
6798
|
-
return;
|
|
6799
|
-
}
|
|
6800
|
-
this.stateRestored = true;
|
|
6801
|
-
this.restoreState();
|
|
6802
|
-
}
|
|
6803
|
-
restoreState() {
|
|
6804
|
-
this.localStorageManager.store$(TRACKER_STORE_NAME)
|
|
6805
|
-
.pipe(take(1))
|
|
6806
|
-
.subscribe({
|
|
6807
|
-
next: (storedState) => {
|
|
6808
|
-
if (this.isTrackerState(storedState)) {
|
|
6809
|
-
this.state = QueryTrackerStateModel.adapt(storedState);
|
|
6810
|
-
this.cleanupExpiredEntries();
|
|
6811
|
-
}
|
|
6812
|
-
else {
|
|
6813
|
-
this.state = { paths: {} };
|
|
6814
|
-
}
|
|
6815
|
-
this.ready$.next(true);
|
|
6816
|
-
},
|
|
6817
|
-
error: () => {
|
|
6818
|
-
this.state = { paths: {} };
|
|
6819
|
-
this.ready$.next(true);
|
|
6820
|
-
}
|
|
6821
|
-
});
|
|
6822
|
-
}
|
|
6823
|
-
persistState() {
|
|
6824
|
-
this.localStorageManager.storeExists$(TRACKER_STORE_NAME)
|
|
6825
|
-
.pipe(take(1))
|
|
6826
|
-
.subscribe((exists) => {
|
|
6827
|
-
if (exists) {
|
|
6828
|
-
this.localStorageManager.updateStore({ name: TRACKER_STORE_NAME, data: this.state });
|
|
6829
|
-
return;
|
|
6830
|
-
}
|
|
6831
|
-
this.localStorageManager.createStore({
|
|
6832
|
-
name: TRACKER_STORE_NAME,
|
|
6833
|
-
data: this.state,
|
|
6834
|
-
options: DEFAULT_TRACKER_OPTIONS,
|
|
6835
|
-
});
|
|
6836
|
-
});
|
|
6837
|
-
}
|
|
6838
|
-
stringifyQuery(query) {
|
|
6839
|
-
return JSON.stringify(Object.keys(query)
|
|
6840
|
-
.sort()
|
|
6841
|
-
.reduce((acc, key) => {
|
|
6842
|
-
acc[key] = query[key];
|
|
6843
|
-
return acc;
|
|
6844
|
-
}, {}));
|
|
6845
|
-
}
|
|
6846
|
-
isTrackerState(value) {
|
|
6847
|
-
return this.isPlainObject(value) && this.isPlainObject(value.paths);
|
|
6848
|
-
}
|
|
6849
|
-
isPlainObject(value) {
|
|
6850
|
-
return Object.prototype.toString.call(value) === '[object Object]';
|
|
6851
|
-
}
|
|
6852
|
-
hasSessionStorage() {
|
|
6853
|
-
try {
|
|
6854
|
-
return typeof sessionStorage !== 'undefined';
|
|
6855
|
-
}
|
|
6856
|
-
catch {
|
|
6857
|
-
return false;
|
|
6858
|
-
}
|
|
6859
|
-
}
|
|
6860
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: QueryParamsTrackerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
6861
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: QueryParamsTrackerService, providedIn: 'root' }); }
|
|
6862
|
-
}
|
|
6863
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: QueryParamsTrackerService, decorators: [{
|
|
6864
|
-
type: Injectable,
|
|
6865
|
-
args: [{
|
|
6866
|
-
providedIn: 'root'
|
|
6867
|
-
}]
|
|
6868
|
-
}] });
|
|
6869
|
-
|
|
6527
|
+
// import { QueryParamsTrackerService } from '../utils/query-params-tracker.service';
|
|
6870
6528
|
const API_OPTS = new InjectionToken('API_OPTS');
|
|
6871
6529
|
/**
|
|
6872
6530
|
* Channel type enum for different communication purposes
|
|
@@ -6909,7 +6567,7 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
6909
6567
|
this.localStorageManagerService = inject(LocalStorageManagerService);
|
|
6910
6568
|
this.utils = inject(UtilsService);
|
|
6911
6569
|
this.logger = inject(LoggerService);
|
|
6912
|
-
|
|
6570
|
+
// private queryParamsTrackerService = inject(QueryParamsTrackerService)
|
|
6913
6571
|
this.error$ = this.httpManagerService.error$;
|
|
6914
6572
|
this.isPending$ = this.httpManagerService.isPending$.pipe(delay(1));
|
|
6915
6573
|
this.operationSuccess = new BehaviorSubject(null);
|
|
@@ -7363,11 +7021,7 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
7363
7021
|
name: tableName,
|
|
7364
7022
|
data: { ...(storeData || {}), ...this.databaseOptions, expires: this.utils.expires(this.databaseOptions.expiresIn) }
|
|
7365
7023
|
});
|
|
7366
|
-
const
|
|
7367
|
-
const schemaChanged = !!storedSchemaSignature && storedSchemaSignature !== schemaSignature;
|
|
7368
|
-
const ensureTable$ = schemaChanged
|
|
7369
|
-
? this.dbManagerService.clearTable(tableName).pipe(switchMap(() => this.dbManagerService.createDatabaseTable(tableDef)))
|
|
7370
|
-
: this.dbManagerService.createDatabaseTable(tableDef);
|
|
7024
|
+
const ensureTable$ = this.dbManagerService.createDatabaseTable(tableDef);
|
|
7371
7025
|
return ensureTable$.pipe(switchMap((created) => {
|
|
7372
7026
|
if (!created) {
|
|
7373
7027
|
console.warn('[DB STORAGE] Table create/open not ready, skipping DB write for this payload:', { table: tableName });
|
|
@@ -7410,7 +7064,7 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
7410
7064
|
}));
|
|
7411
7065
|
}
|
|
7412
7066
|
return this.localStorageManagerService.store$(this.databaseOptions.table).pipe(take(1), switchMap((storeData) => {
|
|
7413
|
-
console.log('[CacheDebug] storeData for table:', this.databaseOptions
|
|
7067
|
+
// console.log('[CacheDebug] storeData for table:', this.databaseOptions!.table, { storeData, requestCache: storeData?.requestCache, expires: storeData?.expires })
|
|
7414
7068
|
const forceRefresh = !!options?.forceRefresh;
|
|
7415
7069
|
const storedSchemaSignature = this.getStoredSchemaSignature(storeData);
|
|
7416
7070
|
const expires = storeData?.expires || 0;
|
|
@@ -7423,12 +7077,12 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
7423
7077
|
}
|
|
7424
7078
|
const expectedSchema = this.buildSchemaFromAdapter();
|
|
7425
7079
|
const expectedSchemaSignature = this.buildSchemaSignature(expectedSchema);
|
|
7426
|
-
console.log('[CacheDebug] schema check:', { tableName: this.databaseOptions
|
|
7080
|
+
// console.log('[CacheDebug] schema check:', { tableName: this.databaseOptions!.table, storedSchemaSignature, expectedSchemaSignature, mismatch: storedSchemaSignature && storedSchemaSignature !== expectedSchemaSignature })
|
|
7427
7081
|
if (storedSchemaSignature && storedSchemaSignature !== expectedSchemaSignature) {
|
|
7428
7082
|
const tableDef = TableSchemaDef.adapt({ table: this.databaseOptions.table, schema: expectedSchema });
|
|
7429
7083
|
return this.dbManagerService.clearTable(this.databaseOptions.table).pipe(switchMap(() => this.dbManagerService.createDatabaseTable(tableDef)), switchMap(() => fetchFromAPI()));
|
|
7430
7084
|
}
|
|
7431
|
-
return this.checkTrackerAllowsRequest(this.databaseOptions.table, this.resolvePath(effectiveParams), options, storeData).pipe(switchMap((trackerAllowsRequest) => {
|
|
7085
|
+
return this.checkTrackerAllowsRequest(this.databaseOptions.table, 'GET', this.resolvePath(effectiveParams), options, storeData).pipe(switchMap((trackerAllowsRequest) => {
|
|
7432
7086
|
if (trackerAllowsRequest) {
|
|
7433
7087
|
const normalizedPath = this.trackerNormalizePath(this.resolvePath(effectiveParams));
|
|
7434
7088
|
if (!normalizedPath.hasQuery) {
|
|
@@ -7460,16 +7114,7 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
7460
7114
|
this.saveRequestCacheMetadata(this.databaseOptions.table, 'GET', requestSignature, storedSchemaSignature ?? expectedSchemaSignature ?? undefined, options);
|
|
7461
7115
|
return of(dbData);
|
|
7462
7116
|
}
|
|
7463
|
-
|
|
7464
|
-
if (Array.isArray(currentStateData) && currentStateData.length > 0) {
|
|
7465
|
-
return of(currentStateData);
|
|
7466
|
-
}
|
|
7467
|
-
if (currentStateData &&
|
|
7468
|
-
!Array.isArray(currentStateData) &&
|
|
7469
|
-
Object.keys(currentStateData).length > 0) {
|
|
7470
|
-
return of(currentStateData);
|
|
7471
|
-
}
|
|
7472
|
-
return of(this.dataType === DataType.ARRAY ? [] : {});
|
|
7117
|
+
return fetchFromAPI();
|
|
7473
7118
|
}), catchError((error) => {
|
|
7474
7119
|
const tableName = this.databaseOptions.table;
|
|
7475
7120
|
console.warn('[DB STORAGE] getTableRecords failed, recreating table and falling back to API:', { table: tableName, error });
|
|
@@ -7661,7 +7306,7 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
7661
7306
|
const tableDef = TableSchemaDef.adapt({ table: this.databaseOptions.table, schema: expectedSchema });
|
|
7662
7307
|
return this.dbManagerService.clearTable(this.databaseOptions.table).pipe(switchMap(() => this.dbManagerService.createDatabaseTable(tableDef)), switchMap(() => fetchStreamFromAPI()));
|
|
7663
7308
|
}
|
|
7664
|
-
return this.checkTrackerAllowsRequest(this.databaseOptions.table, this.resolvePath(effectiveParams), options, storeData).pipe(switchMap((trackerAllowsRequest) => {
|
|
7309
|
+
return this.checkTrackerAllowsRequest(this.databaseOptions.table, 'STREAM', this.resolvePath(effectiveParams), options, storeData).pipe(switchMap((trackerAllowsRequest) => {
|
|
7665
7310
|
if (!trackerAllowsRequest) {
|
|
7666
7311
|
return this.dbManagerService.getTableRecords(this.databaseOptions.table).pipe(switchMap((dbData) => {
|
|
7667
7312
|
if (Array.isArray(dbData) && dbData.length > 0) {
|
|
@@ -7810,6 +7455,17 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
7810
7455
|
this.databaseOptions = (this.hasDatabase) ? adapted : undefined;
|
|
7811
7456
|
// Trigger database table creation if table is configured
|
|
7812
7457
|
if (this.hasDatabase && this.databaseOptions?.table) {
|
|
7458
|
+
this.localStorageManagerService.createStore({
|
|
7459
|
+
name: this.databaseOptions.table,
|
|
7460
|
+
data: {
|
|
7461
|
+
...this.databaseOptions,
|
|
7462
|
+
expires: this.utils.expires(this.databaseOptions.expiresIn),
|
|
7463
|
+
},
|
|
7464
|
+
options: SettingOptions.adapt({
|
|
7465
|
+
storage: StorageType.GLOBAL,
|
|
7466
|
+
encrypted: false,
|
|
7467
|
+
})
|
|
7468
|
+
});
|
|
7813
7469
|
console.log('[setApiRequestOptions] Initializing database storage for table:', this.databaseOptions.table);
|
|
7814
7470
|
// Use initDBStorageAsync directly instead of the effect - effects need subscription to run
|
|
7815
7471
|
this.initDBStorageAsync().subscribe({
|
|
@@ -8019,11 +7675,7 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
8019
7675
|
name: tableName,
|
|
8020
7676
|
data: { ...(storeData || {}), ...this.databaseOptions, expires: this.utils.expires(this.databaseOptions.expiresIn) }
|
|
8021
7677
|
});
|
|
8022
|
-
const
|
|
8023
|
-
const schemaChanged = !!storedSchemaSignature && storedSchemaSignature !== schemaSignature;
|
|
8024
|
-
const ensureTable$ = schemaChanged
|
|
8025
|
-
? this.dbManagerService.clearTable(tableName).pipe(switchMap(() => this.dbManagerService.createDatabaseTable(tableDef)))
|
|
8026
|
-
: this.dbManagerService.createDatabaseTable(tableDef);
|
|
7678
|
+
const ensureTable$ = this.dbManagerService.createDatabaseTable(tableDef);
|
|
8027
7679
|
return ensureTable$.pipe(switchMap((created) => {
|
|
8028
7680
|
if (!created) {
|
|
8029
7681
|
console.warn('[DB STORAGE] Stream table create/open not ready, skipping DB write for this chunk:', { table: tableName });
|
|
@@ -8328,9 +7980,16 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
8328
7980
|
if (!this.hasDatabase || !this.databaseOptions?.table)
|
|
8329
7981
|
return;
|
|
8330
7982
|
const tableName = this.databaseOptions.table;
|
|
7983
|
+
// Reset localStorage tracking SYNCHRONOUSLY before the async DB clear so any
|
|
7984
|
+
// concurrent request that fires during clearTable() sees a clean store immediately
|
|
7985
|
+
// and is not blocked by stale queryParams from the previous session.
|
|
7986
|
+
this.localStorageManagerService.updateStore({
|
|
7987
|
+
name: tableName,
|
|
7988
|
+
data: { ...this.databaseOptions, expires: 0 }
|
|
7989
|
+
});
|
|
7990
|
+
this._requestCachePaths.delete(tableName);
|
|
8331
7991
|
this.dbManagerService.clearTable(tableName).subscribe({
|
|
8332
7992
|
next: () => {
|
|
8333
|
-
this.clearRequestCacheMetadata(tableName);
|
|
8334
7993
|
if (this.dataType === DataType.ARRAY) {
|
|
8335
7994
|
this.setData$([]);
|
|
8336
7995
|
}
|
|
@@ -8451,18 +8110,21 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
8451
8110
|
this._requestCachePaths.set(tableName, this.resolvePath(options?.path).filter(p => typeof p === 'string' || typeof p === 'number').map(String));
|
|
8452
8111
|
this.localStorageManagerService.store$(tableName).pipe(take(1), tap((storeData) => {
|
|
8453
8112
|
const currentCache = storeData?.requestCache || {};
|
|
8113
|
+
const currentEntry = currentCache[type] || {};
|
|
8454
8114
|
this.localStorageManagerService.updateStore({
|
|
8455
8115
|
name: tableName,
|
|
8456
8116
|
data: {
|
|
8457
8117
|
...(storeData || {}),
|
|
8458
|
-
schemaSignature: schemaSignature || storeData?.schemaSignature || null,
|
|
8459
8118
|
requestCache: {
|
|
8460
8119
|
...currentCache,
|
|
8461
8120
|
[type]: {
|
|
8121
|
+
...currentEntry,
|
|
8462
8122
|
signature,
|
|
8463
8123
|
savedAt: Date.now(),
|
|
8464
8124
|
path: this.resolvePath(options?.path),
|
|
8465
|
-
headers: this.filterHeaders(options?.headers)
|
|
8125
|
+
headers: this.filterHeaders(options?.headers),
|
|
8126
|
+
queryParams: currentEntry.queryParams,
|
|
8127
|
+
queryParamsExpires: currentEntry.queryParamsExpires,
|
|
8466
8128
|
}
|
|
8467
8129
|
}
|
|
8468
8130
|
}
|
|
@@ -8482,7 +8144,7 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
8482
8144
|
}
|
|
8483
8145
|
}
|
|
8484
8146
|
if (Array.isArray(pathArray) && pathArray.length > 0) {
|
|
8485
|
-
this.queryParamsTrackerService.clearTrackingForPath(pathArray.join('/'))
|
|
8147
|
+
// this.queryParamsTrackerService.clearTrackingForPath(pathArray.join('/'))
|
|
8486
8148
|
this._requestCachePaths.delete(tableName);
|
|
8487
8149
|
}
|
|
8488
8150
|
if (!storeData)
|
|
@@ -8496,23 +8158,6 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
8496
8158
|
});
|
|
8497
8159
|
})).subscribe();
|
|
8498
8160
|
}
|
|
8499
|
-
getTrackerState(storeData) {
|
|
8500
|
-
if (storeData?.tracker && typeof storeData.tracker === 'object') {
|
|
8501
|
-
return {
|
|
8502
|
-
consumedValuesByKey: storeData.tracker.consumedValuesByKey || {},
|
|
8503
|
-
trackingExpires: storeData.tracker.trackingExpires ?? null,
|
|
8504
|
-
};
|
|
8505
|
-
}
|
|
8506
|
-
return { consumedValuesByKey: {}, trackingExpires: null };
|
|
8507
|
-
}
|
|
8508
|
-
saveTrackerState(tableName, consumedValuesByKey, trackingExpires) {
|
|
8509
|
-
this.localStorageManagerService.store$(tableName).pipe(take(1), tap((storeData) => {
|
|
8510
|
-
this.localStorageManagerService.updateStore({
|
|
8511
|
-
name: tableName,
|
|
8512
|
-
data: { ...(storeData || {}), tracker: { consumedValuesByKey, trackingExpires } }
|
|
8513
|
-
});
|
|
8514
|
-
})).subscribe();
|
|
8515
|
-
}
|
|
8516
8161
|
trackerNormalizePath(path) {
|
|
8517
8162
|
const pathSegments = [];
|
|
8518
8163
|
const query = {};
|
|
@@ -8589,14 +8234,12 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
8589
8234
|
return String(value).trim().toLowerCase();
|
|
8590
8235
|
}
|
|
8591
8236
|
trackerFilterQuery(query, ignoreQueryParams) {
|
|
8592
|
-
if (!Array.isArray(ignoreQueryParams))
|
|
8593
|
-
return { ...query };
|
|
8594
|
-
if (ignoreQueryParams.length === 0)
|
|
8237
|
+
if (!Array.isArray(ignoreQueryParams) || ignoreQueryParams.length === 0)
|
|
8595
8238
|
return { ...query };
|
|
8239
|
+
const normalizedIgnore = ignoreQueryParams.map(p => this.trackerNormalizeParamKey(p));
|
|
8596
8240
|
const result = {};
|
|
8597
|
-
|
|
8598
|
-
|
|
8599
|
-
if (Object.prototype.hasOwnProperty.call(query, key)) {
|
|
8241
|
+
Object.keys(query).forEach((key) => {
|
|
8242
|
+
if (!normalizedIgnore.includes(key)) {
|
|
8600
8243
|
result[key] = query[key];
|
|
8601
8244
|
}
|
|
8602
8245
|
});
|
|
@@ -8618,45 +8261,99 @@ class HTTPManagerStateService extends ComponentStore {
|
|
|
8618
8261
|
return null;
|
|
8619
8262
|
return Math.floor(Date.now() / 1000) + Number(parsed[1]) * seconds;
|
|
8620
8263
|
}
|
|
8621
|
-
checkTrackerAllowsRequest(tableName, path, options, storeData) {
|
|
8622
|
-
if (options && 'watchParams' in options) {
|
|
8623
|
-
return of(this.queryParamsTrackerService.checkRequestOptions(path, {
|
|
8624
|
-
watchParams: options.watchParams,
|
|
8625
|
-
watchExpiresAt: options?.watchExpiresAt,
|
|
8626
|
-
}));
|
|
8627
|
-
}
|
|
8264
|
+
checkTrackerAllowsRequest(tableName, type, path, options, storeData) {
|
|
8628
8265
|
const normalized = this.trackerNormalizePath(path);
|
|
8629
8266
|
const ignoreQueryParams = Array.isArray(options?.ignoreQueryParams) ? options.ignoreQueryParams : [];
|
|
8630
8267
|
if (!normalized.hasQuery) {
|
|
8631
|
-
const meta = this.getRequestCacheMetadata(storeData,
|
|
8268
|
+
const meta = this.getRequestCacheMetadata(storeData, type);
|
|
8269
|
+
if (!meta) {
|
|
8270
|
+
// No prior cache entry — record that we're tracking this request
|
|
8271
|
+
this.setCachedRequestSignature(tableName, type, '');
|
|
8272
|
+
this.localStorageManagerService.store$(tableName).pipe(take(1), tap((s) => {
|
|
8273
|
+
const currentCache = s?.requestCache || {};
|
|
8274
|
+
this.localStorageManagerService.updateStore({
|
|
8275
|
+
name: tableName,
|
|
8276
|
+
data: {
|
|
8277
|
+
...(s || {}),
|
|
8278
|
+
requestCache: {
|
|
8279
|
+
...currentCache,
|
|
8280
|
+
[type]: {
|
|
8281
|
+
...(currentCache[type] || {}),
|
|
8282
|
+
active: true,
|
|
8283
|
+
}
|
|
8284
|
+
}
|
|
8285
|
+
}
|
|
8286
|
+
});
|
|
8287
|
+
})).subscribe();
|
|
8288
|
+
}
|
|
8632
8289
|
return of(!meta);
|
|
8633
8290
|
}
|
|
8634
8291
|
const filtered = this.trackerFilterQuery(normalized.query, ignoreQueryParams);
|
|
8635
8292
|
const keys = Object.keys(filtered);
|
|
8636
8293
|
if (keys.length === 0) {
|
|
8637
|
-
|
|
8294
|
+
// All query params were filtered out — check if we have any prior entry
|
|
8295
|
+
const meta = this.getRequestCacheMetadata(storeData, type);
|
|
8296
|
+
if (!meta) {
|
|
8297
|
+
this.setCachedRequestSignature(tableName, type, '');
|
|
8298
|
+
this.localStorageManagerService.store$(tableName).pipe(take(1), tap((s) => {
|
|
8299
|
+
const currentCache = s?.requestCache || {};
|
|
8300
|
+
this.localStorageManagerService.updateStore({
|
|
8301
|
+
name: tableName,
|
|
8302
|
+
data: {
|
|
8303
|
+
...(s || {}),
|
|
8304
|
+
requestCache: {
|
|
8305
|
+
...currentCache,
|
|
8306
|
+
[type]: {
|
|
8307
|
+
...(currentCache[type] || {}),
|
|
8308
|
+
active: true,
|
|
8309
|
+
}
|
|
8310
|
+
}
|
|
8311
|
+
}
|
|
8312
|
+
});
|
|
8313
|
+
})).subscribe();
|
|
8314
|
+
}
|
|
8315
|
+
return of(!meta);
|
|
8638
8316
|
}
|
|
8639
|
-
const
|
|
8317
|
+
const meta = this.getRequestCacheMetadata(storeData, type) || {};
|
|
8640
8318
|
const now = Math.floor(Date.now() / 1000);
|
|
8641
|
-
|
|
8642
|
-
|
|
8643
|
-
|
|
8319
|
+
let queryParams = { ...(meta.queryParams || {}) };
|
|
8320
|
+
let queryParamsExpires = meta.queryParamsExpires ?? null;
|
|
8321
|
+
if (queryParamsExpires !== null && queryParamsExpires <= now) {
|
|
8322
|
+
queryParams = {};
|
|
8323
|
+
queryParamsExpires = null;
|
|
8644
8324
|
}
|
|
8645
8325
|
let accepted = false;
|
|
8646
8326
|
keys.forEach(key => {
|
|
8647
8327
|
const value = filtered[key];
|
|
8648
|
-
const consumed =
|
|
8328
|
+
const consumed = queryParams[key] || [];
|
|
8649
8329
|
if (!consumed.includes(value)) {
|
|
8650
|
-
|
|
8330
|
+
queryParams[key] = [...consumed, value];
|
|
8651
8331
|
accepted = true;
|
|
8652
8332
|
}
|
|
8653
8333
|
});
|
|
8654
8334
|
if (accepted) {
|
|
8655
8335
|
const newExpiry = this.trackerBuildExpiryEpoch(options?.queryParamsExpiresIn);
|
|
8656
8336
|
if (newExpiry !== null) {
|
|
8657
|
-
|
|
8658
|
-
}
|
|
8659
|
-
this.
|
|
8337
|
+
queryParamsExpires = newExpiry;
|
|
8338
|
+
}
|
|
8339
|
+
this.localStorageManagerService.store$(tableName).pipe(take(1), tap((latestStoreData) => {
|
|
8340
|
+
const currentCache = latestStoreData?.requestCache || {};
|
|
8341
|
+
const currentEntry = currentCache[type] || {};
|
|
8342
|
+
this.localStorageManagerService.updateStore({
|
|
8343
|
+
name: tableName,
|
|
8344
|
+
data: {
|
|
8345
|
+
...(latestStoreData || {}),
|
|
8346
|
+
requestCache: {
|
|
8347
|
+
...currentCache,
|
|
8348
|
+
[type]: {
|
|
8349
|
+
...currentEntry,
|
|
8350
|
+
queryParams,
|
|
8351
|
+
queryParamsExpires,
|
|
8352
|
+
}
|
|
8353
|
+
}
|
|
8354
|
+
}
|
|
8355
|
+
});
|
|
8356
|
+
})).subscribe();
|
|
8660
8357
|
}
|
|
8661
8358
|
return of(accepted);
|
|
8662
8359
|
}
|
|
@@ -8978,6 +8675,673 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
8978
8675
|
}]
|
|
8979
8676
|
}], ctorParameters: () => [] });
|
|
8980
8677
|
|
|
8678
|
+
class SqlParseError extends Error {
|
|
8679
|
+
constructor(message) {
|
|
8680
|
+
super(message);
|
|
8681
|
+
this.name = 'SqlParseError';
|
|
8682
|
+
}
|
|
8683
|
+
}
|
|
8684
|
+
class SqlValidationError extends Error {
|
|
8685
|
+
constructor(message) {
|
|
8686
|
+
super(message);
|
|
8687
|
+
this.name = 'SqlValidationError';
|
|
8688
|
+
}
|
|
8689
|
+
}
|
|
8690
|
+
|
|
8691
|
+
class SqlParser {
|
|
8692
|
+
constructor() {
|
|
8693
|
+
this._parser = new Parser();
|
|
8694
|
+
}
|
|
8695
|
+
parse(sql) {
|
|
8696
|
+
let result;
|
|
8697
|
+
try {
|
|
8698
|
+
result = this._parser.parse(sql);
|
|
8699
|
+
}
|
|
8700
|
+
catch (e) {
|
|
8701
|
+
throw new SqlParseError(e.message ?? 'Failed to parse SQL');
|
|
8702
|
+
}
|
|
8703
|
+
const ast = Array.isArray(result.ast) ? result.ast[0] : result.ast;
|
|
8704
|
+
if (!ast) {
|
|
8705
|
+
throw new SqlParseError('SQL produced no AST — only SELECT statements are supported');
|
|
8706
|
+
}
|
|
8707
|
+
if (ast.type !== 'select') {
|
|
8708
|
+
throw new SqlParseError('Only SELECT statements are supported');
|
|
8709
|
+
}
|
|
8710
|
+
return {
|
|
8711
|
+
ast,
|
|
8712
|
+
tableList: result.tableList ?? [],
|
|
8713
|
+
columnList: result.columnList ?? [],
|
|
8714
|
+
};
|
|
8715
|
+
}
|
|
8716
|
+
}
|
|
8717
|
+
|
|
8718
|
+
class SchemaValidator {
|
|
8719
|
+
constructor() {
|
|
8720
|
+
this._dbManager = inject(DatabaseManagerService);
|
|
8721
|
+
}
|
|
8722
|
+
async validate(parsed, options = {}) {
|
|
8723
|
+
const { ast } = parsed;
|
|
8724
|
+
const fromItems = ast.from ?? [];
|
|
8725
|
+
// Build alias map and collect real table names
|
|
8726
|
+
const aliases = {};
|
|
8727
|
+
const tableNames = [];
|
|
8728
|
+
for (const item of fromItems) {
|
|
8729
|
+
if (item.table) {
|
|
8730
|
+
tableNames.push(item.table);
|
|
8731
|
+
aliases[item.as ?? item.table] = item.table;
|
|
8732
|
+
aliases[item.table] = item.table;
|
|
8733
|
+
}
|
|
8734
|
+
}
|
|
8735
|
+
if (tableNames.length === 0) {
|
|
8736
|
+
throw new SqlValidationError('No tables found in query');
|
|
8737
|
+
}
|
|
8738
|
+
// Fetch and parse schema for each table
|
|
8739
|
+
const schemas = {};
|
|
8740
|
+
for (const tableName of tableNames) {
|
|
8741
|
+
const schemaArr = await firstValueFrom(this._dbManager.getDatabaseTableSchema(tableName));
|
|
8742
|
+
if (!schemaArr || schemaArr.length === 0) {
|
|
8743
|
+
throw new SqlValidationError(`Table not found: "${tableName}"`);
|
|
8744
|
+
}
|
|
8745
|
+
schemas[tableName] = this._parseSchemaArray(schemaArr);
|
|
8746
|
+
}
|
|
8747
|
+
const defaultTable = tableNames[0];
|
|
8748
|
+
// Validate WHERE columns
|
|
8749
|
+
if (ast.where) {
|
|
8750
|
+
this._validateWhereNode(ast.where, defaultTable, aliases, schemas);
|
|
8751
|
+
}
|
|
8752
|
+
// Validate ORDER BY columns
|
|
8753
|
+
for (const ob of (ast.orderby ?? [])) {
|
|
8754
|
+
if (ob.expr?.type === 'column_ref') {
|
|
8755
|
+
const tbl = this._resolveTable(ob.expr.table, defaultTable, aliases);
|
|
8756
|
+
this._assertIndexed(ob.expr.column, tbl, schemas);
|
|
8757
|
+
}
|
|
8758
|
+
}
|
|
8759
|
+
// Validate JOIN ON columns
|
|
8760
|
+
for (const item of fromItems) {
|
|
8761
|
+
if (item.join && item.on) {
|
|
8762
|
+
this._validateJoinOn(item.on, tableNames, aliases, schemas);
|
|
8763
|
+
}
|
|
8764
|
+
}
|
|
8765
|
+
// Strict mode: AND without compound index → throw
|
|
8766
|
+
if (options.strict && ast.where) {
|
|
8767
|
+
this._checkStrictAnd(ast.where, defaultTable, aliases, schemas);
|
|
8768
|
+
}
|
|
8769
|
+
return { ast, schemas, aliases };
|
|
8770
|
+
}
|
|
8771
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────
|
|
8772
|
+
_parseSchemaArray(schemaArr) {
|
|
8773
|
+
const pk = schemaArr[0] ?? '';
|
|
8774
|
+
const simpleIndexes = [];
|
|
8775
|
+
const compoundIndexes = [];
|
|
8776
|
+
for (let i = 1; i < schemaArr.length; i++) {
|
|
8777
|
+
const s = schemaArr[i];
|
|
8778
|
+
if (s.startsWith('[') && s.endsWith(']')) {
|
|
8779
|
+
compoundIndexes.push(s.slice(1, -1).split('+'));
|
|
8780
|
+
}
|
|
8781
|
+
else {
|
|
8782
|
+
simpleIndexes.push(s.replace(/^[*&]/, ''));
|
|
8783
|
+
}
|
|
8784
|
+
}
|
|
8785
|
+
return { pk, simpleIndexes, compoundIndexes };
|
|
8786
|
+
}
|
|
8787
|
+
_isIndexed(col, tableName, schemas) {
|
|
8788
|
+
const schema = schemas[tableName];
|
|
8789
|
+
if (!schema)
|
|
8790
|
+
return false;
|
|
8791
|
+
return col === schema.pk || schema.simpleIndexes.includes(col);
|
|
8792
|
+
}
|
|
8793
|
+
_assertIndexed(col, tableName, schemas) {
|
|
8794
|
+
if (!this._isIndexed(col, tableName, schemas)) {
|
|
8795
|
+
throw new SqlValidationError(`Column "${col}" on table "${tableName}" is not indexed`);
|
|
8796
|
+
}
|
|
8797
|
+
}
|
|
8798
|
+
_resolveTable(tableRef, defaultTable, aliases) {
|
|
8799
|
+
if (!tableRef)
|
|
8800
|
+
return defaultTable;
|
|
8801
|
+
return aliases[tableRef] ?? tableRef;
|
|
8802
|
+
}
|
|
8803
|
+
_validateWhereNode(node, defaultTable, aliases, schemas) {
|
|
8804
|
+
if (!node || node.type !== 'binary_expr')
|
|
8805
|
+
return;
|
|
8806
|
+
if (node.operator === 'AND' || node.operator === 'OR') {
|
|
8807
|
+
this._validateWhereNode(node.left, defaultTable, aliases, schemas);
|
|
8808
|
+
this._validateWhereNode(node.right, defaultTable, aliases, schemas);
|
|
8809
|
+
return;
|
|
8810
|
+
}
|
|
8811
|
+
if (node.left?.type === 'column_ref') {
|
|
8812
|
+
const tbl = this._resolveTable(node.left.table, defaultTable, aliases);
|
|
8813
|
+
this._assertIndexed(node.left.column, tbl, schemas);
|
|
8814
|
+
}
|
|
8815
|
+
}
|
|
8816
|
+
_validateJoinOn(onNode, tableNames, aliases, schemas) {
|
|
8817
|
+
if (!onNode || onNode.type !== 'binary_expr' || onNode.operator !== '=')
|
|
8818
|
+
return;
|
|
8819
|
+
const l = onNode.left;
|
|
8820
|
+
const r = onNode.right;
|
|
8821
|
+
if (l?.type !== 'column_ref' || r?.type !== 'column_ref')
|
|
8822
|
+
return;
|
|
8823
|
+
const leftTable = tableNames[0];
|
|
8824
|
+
const rightTable = tableNames[tableNames.length - 1];
|
|
8825
|
+
// Resolve which ON side belongs to left vs right table
|
|
8826
|
+
const lTableResolved = aliases[l.table ?? ''] ?? l.table ?? leftTable;
|
|
8827
|
+
const rTableResolved = aliases[r.table ?? ''] ?? r.table ?? rightTable;
|
|
8828
|
+
const leftKey = lTableResolved === leftTable ? { col: l.column, table: lTableResolved } : { col: r.column, table: rTableResolved };
|
|
8829
|
+
const rightKey = lTableResolved === leftTable ? { col: r.column, table: rTableResolved } : { col: l.column, table: lTableResolved };
|
|
8830
|
+
// Left-side key: PK is exempt, otherwise must be indexed
|
|
8831
|
+
const leftSchema = schemas[leftKey.table];
|
|
8832
|
+
if (leftSchema && leftKey.col !== leftSchema.pk && !leftSchema.simpleIndexes.includes(leftKey.col)) {
|
|
8833
|
+
throw new SqlValidationError(`JOIN column "${leftKey.col}" on table "${leftKey.table}" must be indexed`);
|
|
8834
|
+
}
|
|
8835
|
+
// Right-side key: must be indexed (including PK counts)
|
|
8836
|
+
if (!this._isIndexed(rightKey.col, rightKey.table, schemas)) {
|
|
8837
|
+
throw new SqlValidationError(`JOIN column "${rightKey.col}" on table "${rightKey.table}" must be indexed`);
|
|
8838
|
+
}
|
|
8839
|
+
}
|
|
8840
|
+
_checkStrictAnd(whereNode, defaultTable, aliases, schemas) {
|
|
8841
|
+
if (!whereNode || whereNode.type !== 'binary_expr' || whereNode.operator !== 'AND')
|
|
8842
|
+
return;
|
|
8843
|
+
const leafCols = this._flattenAndCols(whereNode, defaultTable, aliases);
|
|
8844
|
+
if (leafCols.length < 2)
|
|
8845
|
+
return;
|
|
8846
|
+
const tableName = leafCols[0].table;
|
|
8847
|
+
const schema = schemas[tableName];
|
|
8848
|
+
if (!schema)
|
|
8849
|
+
return;
|
|
8850
|
+
const colNames = leafCols.map(c => c.col);
|
|
8851
|
+
const hasCompound = schema.compoundIndexes.some(ci => ci.length === colNames.length && colNames.every(c => ci.includes(c)));
|
|
8852
|
+
if (!hasCompound) {
|
|
8853
|
+
throw new SqlValidationError(`Strict mode: compound index required for AND on columns [${colNames.join(', ')}]`);
|
|
8854
|
+
}
|
|
8855
|
+
}
|
|
8856
|
+
_flattenAndCols(node, defaultTable, aliases) {
|
|
8857
|
+
if (!node || node.type !== 'binary_expr')
|
|
8858
|
+
return [];
|
|
8859
|
+
if (node.operator === 'AND') {
|
|
8860
|
+
return [
|
|
8861
|
+
...this._flattenAndCols(node.left, defaultTable, aliases),
|
|
8862
|
+
...this._flattenAndCols(node.right, defaultTable, aliases),
|
|
8863
|
+
];
|
|
8864
|
+
}
|
|
8865
|
+
if (node.left?.type === 'column_ref') {
|
|
8866
|
+
return [{ table: this._resolveTable(node.left.table, defaultTable, aliases), col: node.left.column }];
|
|
8867
|
+
}
|
|
8868
|
+
return [];
|
|
8869
|
+
}
|
|
8870
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SchemaValidator, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
8871
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SchemaValidator, providedIn: 'root' }); }
|
|
8872
|
+
}
|
|
8873
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SchemaValidator, decorators: [{
|
|
8874
|
+
type: Injectable,
|
|
8875
|
+
args: [{ providedIn: 'root' }]
|
|
8876
|
+
}] });
|
|
8877
|
+
|
|
8878
|
+
var ExecutionPlanType;
|
|
8879
|
+
(function (ExecutionPlanType) {
|
|
8880
|
+
ExecutionPlanType["SIMPLE"] = "SIMPLE";
|
|
8881
|
+
ExecutionPlanType["AND_BOUNDED"] = "AND_BOUNDED";
|
|
8882
|
+
ExecutionPlanType["AND_COMPOUND"] = "AND_COMPOUND";
|
|
8883
|
+
ExecutionPlanType["OR_MERGE"] = "OR_MERGE";
|
|
8884
|
+
ExecutionPlanType["JOIN_HASH"] = "JOIN_HASH";
|
|
8885
|
+
})(ExecutionPlanType || (ExecutionPlanType = {}));
|
|
8886
|
+
|
|
8887
|
+
class QueryPlanner {
|
|
8888
|
+
plan(validated, options = {}) {
|
|
8889
|
+
const { ast, schemas, aliases } = validated;
|
|
8890
|
+
const fromItems = ast.from ?? [];
|
|
8891
|
+
const mainTable = fromItems[0]?.table ?? '';
|
|
8892
|
+
const dedupeKey = schemas[mainTable]?.pk ?? 'id';
|
|
8893
|
+
const basePlan = this._buildBasePlanExtras(ast, mainTable, dedupeKey);
|
|
8894
|
+
// ── JOIN ────────────────────────────────────────────────────────────────
|
|
8895
|
+
const joinItem = fromItems.find((f) => f.join);
|
|
8896
|
+
if (joinItem) {
|
|
8897
|
+
return this._buildJoinPlan(ast, fromItems, joinItem, schemas, aliases, mainTable, basePlan);
|
|
8898
|
+
}
|
|
8899
|
+
const where = ast.where;
|
|
8900
|
+
// ── No WHERE ─────────────────────────────────────────────────────────────
|
|
8901
|
+
if (!where) {
|
|
8902
|
+
return { ...basePlan, type: ExecutionPlanType.SIMPLE, primaryStep: null, boundedFilters: [] };
|
|
8903
|
+
}
|
|
8904
|
+
// ── OR ──────────────────────────────────────────────────────────────────
|
|
8905
|
+
if (where.operator === 'OR') {
|
|
8906
|
+
return this._buildOrPlan(where, basePlan, aliases, mainTable);
|
|
8907
|
+
}
|
|
8908
|
+
// ── AND ──────────────────────────────────────────────────────────────────
|
|
8909
|
+
if (where.operator === 'AND') {
|
|
8910
|
+
return this._buildAndPlan(where, basePlan, schemas, aliases, mainTable, options);
|
|
8911
|
+
}
|
|
8912
|
+
// ── Single condition ────────────────────────────────────────────────────
|
|
8913
|
+
const step = this._nodeToStep(where, aliases, mainTable);
|
|
8914
|
+
return { ...basePlan, type: ExecutionPlanType.SIMPLE, primaryStep: step, boundedFilters: [] };
|
|
8915
|
+
}
|
|
8916
|
+
// ─── AND ──────────────────────────────────────────────────────────────────
|
|
8917
|
+
_buildAndPlan(where, basePlan, schemas, aliases, mainTable, _options) {
|
|
8918
|
+
const conditions = this._flattenAnd(where);
|
|
8919
|
+
const cols = conditions.map((c) => c.left?.column).filter(Boolean);
|
|
8920
|
+
const schema = schemas[mainTable];
|
|
8921
|
+
// Check for matching compound index
|
|
8922
|
+
const matchedCompound = schema?.compoundIndexes.find(ci => ci.length === cols.length && cols.every(c => ci.includes(c)));
|
|
8923
|
+
if (matchedCompound) {
|
|
8924
|
+
const orderedValues = matchedCompound.map(ciCol => {
|
|
8925
|
+
const cond = conditions.find((c) => c.left?.column === ciCol);
|
|
8926
|
+
return this._extractValue(cond.right, cond.operator);
|
|
8927
|
+
});
|
|
8928
|
+
return {
|
|
8929
|
+
...basePlan,
|
|
8930
|
+
type: ExecutionPlanType.AND_COMPOUND,
|
|
8931
|
+
primaryStep: null,
|
|
8932
|
+
boundedFilters: [],
|
|
8933
|
+
compoundIndex: `[${matchedCompound.join('+')}]`,
|
|
8934
|
+
compoundValues: orderedValues,
|
|
8935
|
+
};
|
|
8936
|
+
}
|
|
8937
|
+
// Pragmatic: first condition → Dexie, rest → JS filter
|
|
8938
|
+
const primaryStep = this._nodeToStep(conditions[0], aliases, mainTable);
|
|
8939
|
+
const boundedFilters = conditions.slice(1).map((c) => this._nodeToStep(c, aliases, mainTable));
|
|
8940
|
+
return {
|
|
8941
|
+
...basePlan,
|
|
8942
|
+
type: ExecutionPlanType.AND_BOUNDED,
|
|
8943
|
+
primaryStep,
|
|
8944
|
+
boundedFilters,
|
|
8945
|
+
compoundWarnCols: cols,
|
|
8946
|
+
};
|
|
8947
|
+
}
|
|
8948
|
+
// ─── OR ───────────────────────────────────────────────────────────────────
|
|
8949
|
+
_buildOrPlan(where, basePlan, aliases, mainTable) {
|
|
8950
|
+
const branches = this._flattenOr(where);
|
|
8951
|
+
const orBranches = branches.map((b) => this._nodeToStep(b, aliases, mainTable));
|
|
8952
|
+
return {
|
|
8953
|
+
...basePlan,
|
|
8954
|
+
type: ExecutionPlanType.OR_MERGE,
|
|
8955
|
+
primaryStep: null,
|
|
8956
|
+
boundedFilters: [],
|
|
8957
|
+
orBranches,
|
|
8958
|
+
};
|
|
8959
|
+
}
|
|
8960
|
+
// ─── JOIN ─────────────────────────────────────────────────────────────────
|
|
8961
|
+
_buildJoinPlan(ast, fromItems, joinItem, schemas, aliases, mainTable, basePlan) {
|
|
8962
|
+
const rightTable = joinItem.table;
|
|
8963
|
+
const onNode = joinItem.on;
|
|
8964
|
+
const joinType = joinItem.join.toLowerCase().includes('left') ? 'left' : 'inner';
|
|
8965
|
+
// Resolve join keys from ON clause
|
|
8966
|
+
const l = onNode?.left;
|
|
8967
|
+
const r = onNode?.right;
|
|
8968
|
+
const lTableResolved = aliases[l?.table ?? ''] ?? l?.table ?? mainTable;
|
|
8969
|
+
const rTableResolved = aliases[r?.table ?? ''] ?? r?.table ?? rightTable;
|
|
8970
|
+
let leftKey, rightKey;
|
|
8971
|
+
if (lTableResolved === mainTable) {
|
|
8972
|
+
leftKey = l?.column ?? '';
|
|
8973
|
+
rightKey = r?.column ?? '';
|
|
8974
|
+
}
|
|
8975
|
+
else {
|
|
8976
|
+
leftKey = r?.column ?? '';
|
|
8977
|
+
rightKey = l?.column ?? '';
|
|
8978
|
+
}
|
|
8979
|
+
const joinInfo = { leftTable: mainTable, rightTable, leftKey, rightKey, joinType };
|
|
8980
|
+
// WHERE conditions become post-join bounded filters
|
|
8981
|
+
const where = ast.where;
|
|
8982
|
+
const boundedFilters = [];
|
|
8983
|
+
if (where) {
|
|
8984
|
+
this._flattenAnd(where.operator === 'AND' ? where : { operator: 'AND', left: where, right: null })
|
|
8985
|
+
.filter(Boolean)
|
|
8986
|
+
.forEach((c) => boundedFilters.push(this._nodeToStep(c, aliases, mainTable)));
|
|
8987
|
+
}
|
|
8988
|
+
return {
|
|
8989
|
+
...basePlan,
|
|
8990
|
+
type: ExecutionPlanType.JOIN_HASH,
|
|
8991
|
+
primaryStep: null,
|
|
8992
|
+
boundedFilters: where ? (where.operator === 'AND' ? boundedFilters : [this._nodeToStep(where, aliases, mainTable)]) : [],
|
|
8993
|
+
joinInfo,
|
|
8994
|
+
};
|
|
8995
|
+
}
|
|
8996
|
+
// ─── Base plan extras (ORDER BY, LIMIT, OFFSET, projection, etc.) ─────────
|
|
8997
|
+
_buildBasePlanExtras(ast, mainTable, dedupeKey) {
|
|
8998
|
+
const plan = { table: mainTable, dedupeKey };
|
|
8999
|
+
// ORDER BY
|
|
9000
|
+
const orderby = ast.orderby ?? [];
|
|
9001
|
+
if (orderby.length > 0) {
|
|
9002
|
+
const ob = orderby[0];
|
|
9003
|
+
plan.orderBy = {
|
|
9004
|
+
col: ob.expr?.column ?? '',
|
|
9005
|
+
dir: ob.type?.toUpperCase() === 'DESC' ? 'desc' : 'asc',
|
|
9006
|
+
};
|
|
9007
|
+
}
|
|
9008
|
+
// LIMIT / OFFSET
|
|
9009
|
+
if (ast.limit) {
|
|
9010
|
+
const vals = ast.limit.value ?? [];
|
|
9011
|
+
if (ast.limit.seperator === 'offset' && vals.length === 2) {
|
|
9012
|
+
plan.limit = vals[0].value;
|
|
9013
|
+
plan.offset = vals[1].value;
|
|
9014
|
+
}
|
|
9015
|
+
else if (vals.length === 1) {
|
|
9016
|
+
plan.limit = vals[0].value;
|
|
9017
|
+
}
|
|
9018
|
+
}
|
|
9019
|
+
// Projection
|
|
9020
|
+
const columns = ast.columns;
|
|
9021
|
+
if (columns && columns !== '*') {
|
|
9022
|
+
// COUNT(*)
|
|
9023
|
+
const countCol = columns.find((c) => c.expr?.type === 'aggr_func' && c.expr?.name === 'COUNT');
|
|
9024
|
+
if (countCol) {
|
|
9025
|
+
plan.aggregate = 'count';
|
|
9026
|
+
plan.projection = null;
|
|
9027
|
+
}
|
|
9028
|
+
else {
|
|
9029
|
+
const projected = columns
|
|
9030
|
+
.filter((c) => c.expr?.type === 'column_ref')
|
|
9031
|
+
.map((c) => c.expr.column);
|
|
9032
|
+
plan.projection = projected.length > 0 ? projected : null;
|
|
9033
|
+
}
|
|
9034
|
+
}
|
|
9035
|
+
else {
|
|
9036
|
+
plan.projection = null;
|
|
9037
|
+
}
|
|
9038
|
+
// DISTINCT
|
|
9039
|
+
plan.distinct = ast.distinct === 'DISTINCT';
|
|
9040
|
+
return plan;
|
|
9041
|
+
}
|
|
9042
|
+
// ─── Node → PlanStep ──────────────────────────────────────────────────────
|
|
9043
|
+
_nodeToStep(node, aliases, defaultTable) {
|
|
9044
|
+
if (!node || node.type !== 'binary_expr' || node.left?.type !== 'column_ref') {
|
|
9045
|
+
throw new SqlValidationError(`Unsupported WHERE condition structure`);
|
|
9046
|
+
}
|
|
9047
|
+
const col = node.left.column;
|
|
9048
|
+
const { op, val } = this._mapOperator(node.operator, node.right);
|
|
9049
|
+
return { col, op, val };
|
|
9050
|
+
}
|
|
9051
|
+
_mapOperator(astOp, rightNode) {
|
|
9052
|
+
switch (astOp) {
|
|
9053
|
+
case '=': return { op: 'equals', val: this._extractValue(rightNode, astOp) };
|
|
9054
|
+
case '!=':
|
|
9055
|
+
case '<>': return { op: 'notEqual', val: this._extractValue(rightNode, astOp) };
|
|
9056
|
+
case '>': return { op: 'above', val: this._extractValue(rightNode, astOp) };
|
|
9057
|
+
case '>=': return { op: 'aboveOrEqual', val: this._extractValue(rightNode, astOp) };
|
|
9058
|
+
case '<': return { op: 'below', val: this._extractValue(rightNode, astOp) };
|
|
9059
|
+
case '<=': return { op: 'belowOrEqual', val: this._extractValue(rightNode, astOp) };
|
|
9060
|
+
case 'BETWEEN': {
|
|
9061
|
+
const vals = rightNode?.value ?? [];
|
|
9062
|
+
return { op: 'between', val: [vals[0]?.value, vals[1]?.value] };
|
|
9063
|
+
}
|
|
9064
|
+
case 'IN': return { op: 'anyOf', val: (rightNode?.value ?? []).map((v) => v.value) };
|
|
9065
|
+
case 'NOT IN': return { op: 'noneOf', val: (rightNode?.value ?? []).map((v) => v.value) };
|
|
9066
|
+
case 'LIKE': {
|
|
9067
|
+
const raw = String(rightNode?.value ?? '');
|
|
9068
|
+
if (raw.startsWith('%')) {
|
|
9069
|
+
throw new SqlValidationError('LIKE with leading wildcard is not supported');
|
|
9070
|
+
}
|
|
9071
|
+
if (raw.includes('%')) {
|
|
9072
|
+
const pctIdx = raw.indexOf('%');
|
|
9073
|
+
if (pctIdx !== raw.length - 1) {
|
|
9074
|
+
throw new SqlValidationError('LIKE with leading wildcard is not supported');
|
|
9075
|
+
}
|
|
9076
|
+
return { op: 'startsWith', val: raw.slice(0, -1) };
|
|
9077
|
+
}
|
|
9078
|
+
// No wildcard — treat as equals
|
|
9079
|
+
return { op: 'equals', val: raw };
|
|
9080
|
+
}
|
|
9081
|
+
default:
|
|
9082
|
+
throw new SqlValidationError(`Unsupported SQL operator: "${astOp}"`);
|
|
9083
|
+
}
|
|
9084
|
+
}
|
|
9085
|
+
_extractValue(node, _op) {
|
|
9086
|
+
return node?.value ?? null;
|
|
9087
|
+
}
|
|
9088
|
+
// ─── WHERE tree helpers ───────────────────────────────────────────────────
|
|
9089
|
+
_flattenAnd(node) {
|
|
9090
|
+
if (!node)
|
|
9091
|
+
return [];
|
|
9092
|
+
if (node.type === 'binary_expr' && node.operator === 'AND') {
|
|
9093
|
+
return [...this._flattenAnd(node.left), ...this._flattenAnd(node.right)];
|
|
9094
|
+
}
|
|
9095
|
+
return [node];
|
|
9096
|
+
}
|
|
9097
|
+
_flattenOr(node) {
|
|
9098
|
+
if (!node)
|
|
9099
|
+
return [];
|
|
9100
|
+
if (node.type === 'binary_expr' && node.operator === 'OR') {
|
|
9101
|
+
return [...this._flattenOr(node.left), ...this._flattenOr(node.right)];
|
|
9102
|
+
}
|
|
9103
|
+
return [node];
|
|
9104
|
+
}
|
|
9105
|
+
}
|
|
9106
|
+
|
|
9107
|
+
class DexieQueryExecutor {
|
|
9108
|
+
constructor() {
|
|
9109
|
+
this._dbManager = inject(DatabaseManagerService);
|
|
9110
|
+
}
|
|
9111
|
+
async execute(plan) {
|
|
9112
|
+
switch (plan.type) {
|
|
9113
|
+
case ExecutionPlanType.SIMPLE: return this._executeSimple(plan);
|
|
9114
|
+
case ExecutionPlanType.AND_BOUNDED: return this._executeAndBounded(plan);
|
|
9115
|
+
case ExecutionPlanType.AND_COMPOUND: return this._executeAndCompound(plan);
|
|
9116
|
+
case ExecutionPlanType.OR_MERGE: return this._executeOrMerge(plan);
|
|
9117
|
+
case ExecutionPlanType.JOIN_HASH: return this._executeJoinHash(plan);
|
|
9118
|
+
default:
|
|
9119
|
+
throw new SqlValidationError(`Unknown plan type`);
|
|
9120
|
+
}
|
|
9121
|
+
}
|
|
9122
|
+
// ─── SIMPLE ───────────────────────────────────────────────────────────────
|
|
9123
|
+
async _executeSimple(plan) {
|
|
9124
|
+
const table = await this._getTable(plan.table);
|
|
9125
|
+
let rows;
|
|
9126
|
+
if (!plan.primaryStep) {
|
|
9127
|
+
rows = await table.toArray();
|
|
9128
|
+
}
|
|
9129
|
+
else {
|
|
9130
|
+
rows = await this._applyDexieStep(table, plan.primaryStep);
|
|
9131
|
+
}
|
|
9132
|
+
return this._postProcess(rows, plan);
|
|
9133
|
+
}
|
|
9134
|
+
// ─── AND_BOUNDED ──────────────────────────────────────────────────────────
|
|
9135
|
+
async _executeAndBounded(plan) {
|
|
9136
|
+
const table = await this._getTable(plan.table);
|
|
9137
|
+
if (plan.compoundWarnCols?.length) {
|
|
9138
|
+
console.warn(`[DexieSqlService] Consider adding a compound index [${plan.compoundWarnCols.join('+')}] ` +
|
|
9139
|
+
`to table "${plan.table}" for full index coverage on AND conditions.`);
|
|
9140
|
+
}
|
|
9141
|
+
let rows = plan.primaryStep
|
|
9142
|
+
? await this._applyDexieStep(table, plan.primaryStep)
|
|
9143
|
+
: await table.toArray();
|
|
9144
|
+
// Apply remaining AND conditions as JS filter
|
|
9145
|
+
for (const f of plan.boundedFilters) {
|
|
9146
|
+
rows = rows.filter(row => this._matchStep(row, f));
|
|
9147
|
+
}
|
|
9148
|
+
return this._postProcess(rows, plan);
|
|
9149
|
+
}
|
|
9150
|
+
// ─── AND_COMPOUND ─────────────────────────────────────────────────────────
|
|
9151
|
+
async _executeAndCompound(plan) {
|
|
9152
|
+
const table = await this._getTable(plan.table);
|
|
9153
|
+
const rows = await table.where(plan.compoundIndex).equals(plan.compoundValues).toArray();
|
|
9154
|
+
return this._postProcess(rows, plan);
|
|
9155
|
+
}
|
|
9156
|
+
// ─── OR_MERGE ─────────────────────────────────────────────────────────────
|
|
9157
|
+
async _executeOrMerge(plan) {
|
|
9158
|
+
const table = await this._getTable(plan.table);
|
|
9159
|
+
const branches = plan.orBranches ?? [];
|
|
9160
|
+
const results = await Promise.all(branches.map(branch => this._applyDexieStep(table, branch)));
|
|
9161
|
+
// Deduplicate by primary key
|
|
9162
|
+
const pk = plan.dedupeKey ?? 'id';
|
|
9163
|
+
const seen = new Map();
|
|
9164
|
+
for (const batch of results) {
|
|
9165
|
+
for (const row of batch) {
|
|
9166
|
+
seen.set(row[pk], row);
|
|
9167
|
+
}
|
|
9168
|
+
}
|
|
9169
|
+
return this._postProcess(Array.from(seen.values()), plan);
|
|
9170
|
+
}
|
|
9171
|
+
// ─── JOIN_HASH ────────────────────────────────────────────────────────────
|
|
9172
|
+
async _executeJoinHash(plan) {
|
|
9173
|
+
const join = plan.joinInfo;
|
|
9174
|
+
const leftTable = await this._getTable(join.leftTable);
|
|
9175
|
+
const rightTable = await this._getTable(join.rightTable);
|
|
9176
|
+
// Fetch left rows (full scan; bounded WHERE filters applied post-join)
|
|
9177
|
+
const leftRows = await leftTable.toArray();
|
|
9178
|
+
// Extract FK values for right-side lookup
|
|
9179
|
+
const fkValues = [...new Set(leftRows.map(r => r[join.leftKey]).filter(v => v != null))];
|
|
9180
|
+
// Fetch right rows indexed by join key
|
|
9181
|
+
const rightRows = fkValues.length > 0
|
|
9182
|
+
? await rightTable.where(join.rightKey).anyOf(fkValues).toArray()
|
|
9183
|
+
: [];
|
|
9184
|
+
// Build lookup map from rightKey → array of right rows
|
|
9185
|
+
const rightMap = new Map();
|
|
9186
|
+
for (const rr of rightRows) {
|
|
9187
|
+
const k = rr[join.rightKey];
|
|
9188
|
+
if (!rightMap.has(k))
|
|
9189
|
+
rightMap.set(k, []);
|
|
9190
|
+
rightMap.get(k).push(rr);
|
|
9191
|
+
}
|
|
9192
|
+
// Hash join
|
|
9193
|
+
const joined = [];
|
|
9194
|
+
for (const lr of leftRows) {
|
|
9195
|
+
const matches = rightMap.get(lr[join.leftKey]);
|
|
9196
|
+
if (matches?.length) {
|
|
9197
|
+
for (const rr of matches) {
|
|
9198
|
+
joined.push({ ...lr, ...rr });
|
|
9199
|
+
}
|
|
9200
|
+
}
|
|
9201
|
+
else if (join.joinType === 'left') {
|
|
9202
|
+
joined.push({ ...lr });
|
|
9203
|
+
}
|
|
9204
|
+
}
|
|
9205
|
+
// Apply post-join bounded filters (any WHERE conditions from the query)
|
|
9206
|
+
let result = joined;
|
|
9207
|
+
for (const f of plan.boundedFilters) {
|
|
9208
|
+
result = result.filter(row => this._matchStep(row, f));
|
|
9209
|
+
}
|
|
9210
|
+
return this._postProcess(result, plan);
|
|
9211
|
+
}
|
|
9212
|
+
// ─── Post-processing ──────────────────────────────────────────────────────
|
|
9213
|
+
_postProcess(rows, plan) {
|
|
9214
|
+
let result = rows;
|
|
9215
|
+
// ORDER BY
|
|
9216
|
+
if (plan.orderBy) {
|
|
9217
|
+
const { col, dir } = plan.orderBy;
|
|
9218
|
+
result = [...result].sort((a, b) => {
|
|
9219
|
+
const av = a[col];
|
|
9220
|
+
const bv = b[col];
|
|
9221
|
+
if (av == null && bv == null)
|
|
9222
|
+
return 0;
|
|
9223
|
+
if (av == null)
|
|
9224
|
+
return 1;
|
|
9225
|
+
if (bv == null)
|
|
9226
|
+
return -1;
|
|
9227
|
+
const cmp = av < bv ? -1 : av > bv ? 1 : 0;
|
|
9228
|
+
return dir === 'desc' ? -cmp : cmp;
|
|
9229
|
+
});
|
|
9230
|
+
}
|
|
9231
|
+
// LIMIT / OFFSET
|
|
9232
|
+
const offset = plan.offset ?? 0;
|
|
9233
|
+
const limit = plan.limit ?? null;
|
|
9234
|
+
if (offset > 0 || limit != null) {
|
|
9235
|
+
result = result.slice(offset, limit != null ? offset + limit : undefined);
|
|
9236
|
+
}
|
|
9237
|
+
// Projection
|
|
9238
|
+
if (plan.projection && plan.projection.length > 0) {
|
|
9239
|
+
const cols = plan.projection;
|
|
9240
|
+
result = result.map(row => {
|
|
9241
|
+
const projected = {};
|
|
9242
|
+
for (const c of cols)
|
|
9243
|
+
projected[c] = row[c];
|
|
9244
|
+
return projected;
|
|
9245
|
+
});
|
|
9246
|
+
}
|
|
9247
|
+
// DISTINCT
|
|
9248
|
+
if (plan.distinct) {
|
|
9249
|
+
const seen = new Set();
|
|
9250
|
+
result = result.filter(row => {
|
|
9251
|
+
const key = JSON.stringify(row);
|
|
9252
|
+
if (seen.has(key))
|
|
9253
|
+
return false;
|
|
9254
|
+
seen.add(key);
|
|
9255
|
+
return true;
|
|
9256
|
+
});
|
|
9257
|
+
}
|
|
9258
|
+
// COUNT(*)
|
|
9259
|
+
if (plan.aggregate === 'count') {
|
|
9260
|
+
return [{ count: result.length }];
|
|
9261
|
+
}
|
|
9262
|
+
return result;
|
|
9263
|
+
}
|
|
9264
|
+
// ─── Dexie step dispatch ──────────────────────────────────────────────────
|
|
9265
|
+
_applyDexieStep(table, step) {
|
|
9266
|
+
const col = step.col;
|
|
9267
|
+
const val = step.val;
|
|
9268
|
+
switch (step.op) {
|
|
9269
|
+
case 'equals': return table.where(col).equals(val).toArray();
|
|
9270
|
+
case 'notEqual': return table.where(col).notEqual(val).toArray();
|
|
9271
|
+
case 'above': return table.where(col).above(val).toArray();
|
|
9272
|
+
case 'aboveOrEqual': return table.where(col).aboveOrEqual(val).toArray();
|
|
9273
|
+
case 'below': return table.where(col).below(val).toArray();
|
|
9274
|
+
case 'belowOrEqual': return table.where(col).belowOrEqual(val).toArray();
|
|
9275
|
+
case 'between': return table.where(col).between(val[0], val[1], true, true).toArray();
|
|
9276
|
+
case 'anyOf': return table.where(col).anyOf(val).toArray();
|
|
9277
|
+
case 'noneOf': return table.where(col).noneOf(val).toArray();
|
|
9278
|
+
case 'startsWith': return table.where(col).startsWith(val).toArray();
|
|
9279
|
+
default:
|
|
9280
|
+
throw new SqlValidationError(`Unsupported Dexie op: "${step.op}"`);
|
|
9281
|
+
}
|
|
9282
|
+
}
|
|
9283
|
+
// ─── JS row predicate ─────────────────────────────────────────────────────
|
|
9284
|
+
_matchStep(row, step) {
|
|
9285
|
+
const rv = row[step.col];
|
|
9286
|
+
const v = step.val;
|
|
9287
|
+
switch (step.op) {
|
|
9288
|
+
case 'equals': return rv === v;
|
|
9289
|
+
case 'notEqual': return rv !== v;
|
|
9290
|
+
case 'above': return rv > v;
|
|
9291
|
+
case 'aboveOrEqual': return rv >= v;
|
|
9292
|
+
case 'below': return rv < v;
|
|
9293
|
+
case 'belowOrEqual': return rv <= v;
|
|
9294
|
+
case 'between': return rv >= v[0] && rv <= v[1];
|
|
9295
|
+
case 'anyOf': return v.includes(rv);
|
|
9296
|
+
case 'noneOf': return !v.includes(rv);
|
|
9297
|
+
case 'startsWith': return typeof rv === 'string' && rv.startsWith(v);
|
|
9298
|
+
default: return true;
|
|
9299
|
+
}
|
|
9300
|
+
}
|
|
9301
|
+
// ─── Table accessor ───────────────────────────────────────────────────────
|
|
9302
|
+
async _getTable(tableName) {
|
|
9303
|
+
const t = await firstValueFrom(this._dbManager.getDatabaseTable(tableName));
|
|
9304
|
+
if (!t)
|
|
9305
|
+
throw new SqlValidationError(`Table "${tableName}" not found or not accessible`);
|
|
9306
|
+
return t;
|
|
9307
|
+
}
|
|
9308
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DexieQueryExecutor, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
9309
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DexieQueryExecutor, providedIn: 'root' }); }
|
|
9310
|
+
}
|
|
9311
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DexieQueryExecutor, decorators: [{
|
|
9312
|
+
type: Injectable,
|
|
9313
|
+
args: [{ providedIn: 'root' }]
|
|
9314
|
+
}] });
|
|
9315
|
+
|
|
9316
|
+
class DexieSqlService {
|
|
9317
|
+
constructor() {
|
|
9318
|
+
this._parser = new SqlParser();
|
|
9319
|
+
this._validator = inject(SchemaValidator);
|
|
9320
|
+
this._planner = new QueryPlanner();
|
|
9321
|
+
this._executor = inject(DexieQueryExecutor);
|
|
9322
|
+
}
|
|
9323
|
+
/**
|
|
9324
|
+
* Execute a MySQL-syntax SQL SELECT string against the DexieJS IndexedDB database.
|
|
9325
|
+
* Returns an Observable that emits the result array and completes, or errors with
|
|
9326
|
+
* SqlParseError / SqlValidationError if the query is invalid.
|
|
9327
|
+
*/
|
|
9328
|
+
query(sql, options = {}) {
|
|
9329
|
+
return from(this._run(sql, options));
|
|
9330
|
+
}
|
|
9331
|
+
async _run(sql, options) {
|
|
9332
|
+
const parsed = this._parser.parse(sql);
|
|
9333
|
+
const validated = await this._validator.validate(parsed, options);
|
|
9334
|
+
const plan = this._planner.plan(validated, options);
|
|
9335
|
+
return this._executor.execute(plan);
|
|
9336
|
+
}
|
|
9337
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DexieSqlService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
9338
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DexieSqlService, providedIn: 'root' }); }
|
|
9339
|
+
}
|
|
9340
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DexieSqlService, decorators: [{
|
|
9341
|
+
type: Injectable,
|
|
9342
|
+
args: [{ providedIn: 'root' }]
|
|
9343
|
+
}] });
|
|
9344
|
+
|
|
8981
9345
|
// export * from "./database-manager-services/index";
|
|
8982
9346
|
|
|
8983
9347
|
class ErrorDisplaySettings {
|
|
@@ -10430,11 +10794,11 @@ class RequestManagerStateDemoComponent {
|
|
|
10430
10794
|
this.prompts = [];
|
|
10431
10795
|
}
|
|
10432
10796
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RequestManagerStateDemoComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
10433
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: RequestManagerStateDemoComponent, selector: "app-request-manager-state-demo", inputs: { server: "server", adapter: "adapter", mapper: "mapper" }, providers: [StateManagerDemoService, HTTPManagerService], viewQueries: [{ propertyName: "failedState", first: true, predicate: ["failedState"], descendants: true, static: true }, { propertyName: "pollingState", first: true, predicate: ["pollingState"], descendants: true, static: true }, { propertyName: "DBState", first: true, predicate: ["DBState"], descendants: true, static: true }], ngImport: i0, template: "<div style=\"margin: 2rem;\">\n\n <h2>\n HTTP Request State Manager\n </h2>\n\n <div [formGroup]=\"requestForm\" style=\"margin-top: 2rem;\">\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\">\n <mat-label>State Data Type</mat-label>\n <mat-select formControlName=\"datatype\">\n <mat-option value=\"ARRAY\">Array</mat-option>\n <mat-option value=\"OBJECT\">Object</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Adapter (Model)</mat-label>\n <mat-select formControlName=\"adapter\" #adapterSelect>\n <mat-option>None</mat-option>\n @for (adapter of sampleAdaptors; track adapter) {\n <mat-option [value]=\"adapter.value\">\n {{adapter.label}}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Mapper (Model)</mat-label>\n <mat-select formControlName=\"mapper\" #mapperSelect>\n <mat-option>None</mat-option>\n @for (mapper of sampleMappers; track mapper) {\n <mat-option [value]=\"mapper.value\">\n {{mapper.label}}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n\n @if (adapterSelect.value || mapperSelect.value) {\n <div style=\"display: flex; margin-bottom: 2rem;\">\n <div style=\"flex:1\" class=\"box\">\n <h3>Adapter (Incoming)</h3>\n @if (adapterSelect.value) {\n <div>\n {{ props(adapterSelect.value) | json }}\n </div>\n } @else {\n No Transformation\n }\n </div>\n <div style=\"flex:1\" class=\"box\">\n <h3>Mapper (Outgoing)</h3>\n @if (mapperSelect.value) {\n <div>\n {{ props(mapperSelect.value) | json }}\n </div>\n } @else {\n No Transformation\n }\n </div>\n </div>\n }\n\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\" style=\"flex:1\">\n <mat-label>RestPath (/ delimited)</mat-label>\n <input matInput placeholder=\"clients/list\" formControlName=\"path\">\n </mat-form-field>\n </div>\n <div>\n <div formArrayName=\"headers\">\n @for (task of headers.controls; track task; let i = $index) {\n <div [formGroupName]=\"i\">\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Key</mat-label>\n <input matInput placeholder=\"authentication\" formControlName=\"key\">\n </mat-form-field>\n <mat-form-field appearance=\"outline\" style=\"flex:1\">\n <mat-label>Value</mat-label>\n <input matInput placeholder=\"sample\" formControlName=\"value\">\n </mat-form-field>\n <div style=\"margin-top: .5rem;\">\n <button mat-icon-button (click)=\"removeHeader(i)\">\n <mat-icon>close</mat-icon>\n </button>\n </div>\n </div>\n </div>\n }\n </div>\n <button mat-stroked-button (click)=\"addHeader()\">Add Header</button>\n </div>\n <div style=\"margin-top: 2rem; display: flex; flex-direction:column; gap: 1rem;\">\n <div>\n <mat-slide-toggle #failedState>Retry on Failed</mat-slide-toggle>\n @if (failedState.checked) {\n <div style=\"display: flex; gap: .5rem; margin-top: 1rem;\" formGroupName=\"retry\">\n <mat-form-field appearance=\"outline\">\n <mat-label>#of Times</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"times\" value=\"3\">\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Delay Until Next</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"delay\" value=\"3\">\n </mat-form-field>\n </div>\n }\n </div>\n <div>\n <mat-slide-toggle #pollingState>Polling</mat-slide-toggle>\n @if (pollingState.checked) {\n <div>\n <mat-form-field appearance=\"outline\" style=\"margin-top: 1rem\">\n <mat-label>#of Seconds</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"polling\" value=\"3\">\n </mat-form-field>\n </div>\n }\n </div>\n <div>\n <mat-slide-toggle #DBState>Database Storage</mat-slide-toggle>\n @if (DBState.checked) {\n <div style=\"margin-top: 1rem\" formGroupName=\"database\">\n <div style=\"display: flex; gap: .5rem;\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Table Name</mat-label>\n <input matInput placeholder=\"table\" formControlName=\"table\" value=\"\">\n </mat-form-field>\n <div>\n <mat-form-field appearance=\"outline\">\n <mat-label>Expires In</mat-label>\n <mat-select formControlName=\"expiresIn\">\n <mat-option value=\"1m\">One Minute</mat-option>\n <mat-option value=\"1h\">One Hour</mat-option>\n <mat-option value=\"1d\">One Day</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n </div>\n <div style=\"display: flex; gap: .5rem;\">\n <mat-form-field appearance=\"outline\" style=\"flex:1\">\n <mat-label>Ignore Params (comma delimited)</mat-label>\n <input matInput placeholder=\"sort,active\" formControlName=\"ignoreQueryParams\" value=\"\">\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Watch Param Expiry</mat-label>\n <mat-select formControlName=\"queryParamsExpiresIn\">\n <mat-option value=\"never\">Never</mat-option>\n <mat-option value=\"10s\">10 Seconds</mat-option>\n <mat-option value=\"30s\">30 Seconds</mat-option>\n <mat-option value=\"1m\">1 Minute</mat-option>\n <mat-option value=\"5m\">5 Minutes</mat-option>\n <mat-option value=\"10m\">10 Minutes</mat-option>\n <mat-option value=\"1h\">1 Hour</mat-option>\n <mat-option value=\"1d\">1 Day</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n <div style=\"margin-top: .5rem; display: flex; gap: .5rem;\">\n <button mat-stroked-button color=\"warn\" (click)=\"onClearRecords()\">Clear Table</button>\n <button mat-stroked-button color=\"warn\" (click)=\"deleteDatabase()\">Delete Database</button>\n </div>\n </div>\n }\n </div>\n <div style=\"margin-top: 1rem; display: flex;\">\n <span style=\"flex:1\"></span>\n <button mat-stroked-button (click)=\"onSetStateOptions()\" [disabled]=\"!hasChanged\">\n Set API Request Options\n </button>\n </div>\n <div>\n @if ((error$ | async); as error) {\n <mat-error>\n {{ error }}\n </mat-error>\n }\n </div>\n </div>\n </div>\n\n <div style=\"margin-bottom: 1rem; margin-top: 2rem;\">\n @if ((isPending$ | async)) {\n <mat-progress-bar mode=\"indeterminate\"\n ></mat-progress-bar>\n }\n @if (pollingState.checked) {\n <div>\n @if (!(isPending$ | async) && (this.countdown$ | async) || -1 > 0) {\n <mat-progress-bar mode=\"determinate\"\n [value]=\"(this.countdown$ | async)\"\n ></mat-progress-bar>\n }\n </div>\n }\n\n </div>\n\n @if ((GET$ | async); as data) {\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1;\">CURRENT STATE ({{ dataType }})</h2>\n </div>\n @if (data.length > 1) {\n <div>\n <mat-form-field appearance=\"outline\">\n <mat-label>Records</mat-label>\n <mat-select [formControl]=\"selectedRecord\" [disabled]=\"data === null && data?.length === 0\">\n <mat-option [value]=\"\">None</mat-option>\n @for (item of data; track item) {\n <mat-option [value]=\"item\">\n {{ getRecordLabel(item) | titlecase }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n }\n @if ((dataObservable$ | async); as dataRecord) {\n <div>\n @if ((selectedRecord$ | async); as record) {\n <div>\n {{ record | json }}\n </div>\n } @else {\n No Record Selected from State\n }\n </div>\n }\n <div style=\"margin-top: 1rem;\">\n @if (data !== null && data?.length > 0) {\n <span>\n State contains a Total of {{ data.length }} Records\n </span>\n } @else {\n No Records\n }\n <div style=\"display: flex; gap: .5rem; text-align: end; justify-content: flex-end;\">\n <button class=\"btn\" mat-stroked-button (click)=\"onClearRecords()\">Clear</button>\n </div>\n </div>\n </div>\n }\n\n\n\n\n <div style=\"margin-top: 1rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">GET Request ({{ dataType }})</h2>\n <div>\n <button mat-raised-button (click)=\"onGetRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((GET_error$ | async); as get_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ get_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(GET$ | async) | jsonv\"></div> -->\n @if ((GET$ | async); as getData) {\n <div>{{ getData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">POST Request</h2>\n <div>\n <button mat-raised-button (click)=\"onCreateRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((POST_error$ | async); as post_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ post_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(POST$ | async) | jsonv\"></div> -->\n @if ((POST$ | async); as postData) {\n <div>{{ postData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">PUT Request</h2>\n <div>\n <button mat-raised-button (click)=\"onUpdateRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((PUT_error$ | async); as put_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ put_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(PUT$ | async) | jsonv\"></div> -->\n @if ((PUT$ | async); as putData) {\n <div>{{ putData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">DELETE Request</h2>\n <div>\n <button mat-raised-button (click)=\"onDeleteRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((DELETE_error$ | async); as delete_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ delete_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(DELETE$ | async) | jsonv\"></div> -->\n @if ((DELETE$ | async); as deleteData) {\n <div>{{ deleteData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">Streaming GET Request</h2>\n <div style=\"display: flex; gap: 1rem; align-items: center;\">\n <div style=\"display: flex; gap: 1rem; align-items: center;\">\n {{ streamType }}\n <button mat-icon-button [matMenuTriggerFor]=\"menu\">\n <mat-icon>data_usage</mat-icon>\n </button>\n </div>\n <mat-menu #menu=\"matMenu\">\n <button mat-menu-item *ngFor=\"let item of streamTypes\" (click)=\"onStreamType(item.id)\">{{ item.value }}</button>\n </mat-menu>\n <button mat-raised-button (click)=\"onStreamRequest()\" class=\"btn\" [disabled]=\"hasChanged\">Request</button>\n </div>\n </div>\n\n @if ((STREAM_error$ | async); as stream_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ stream_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n @if ((STREAM$ | async); as data) {\n <div class=\"container\">\n <table mat-table [dataSource]=\"data\" class=\"mat-elevation-z8\">\n\n <!-- Dynamic columns -->\n <ng-container *ngFor=\"let column of displayedColumns\" [matColumnDef]=\"column\">\n <th mat-header-cell *matHeaderCellDef> {{ column | titlecase }} </th>\n <td mat-cell *matCellDef=\"let element\">\n @if (isObject(element[column]); as objValue) {\n <pre style=\"margin: 0; font-size: 0.8em; white-space: pre-wrap;\">{{ objValue | json }}</pre>\n } @else {\n {{ element[column] }}\n }\n </td>\n </ng-container>\n\n <tr mat-header-row *matHeaderRowDef=\"displayedColumns\"></tr>\n <tr mat-row *matRowDef=\"let row; columns: displayedColumns;\"></tr>\n </table>\n\n <!-- Debug info -->\n <div style=\"margin-top: 1rem; font-size: 0.8em; color: #666;\">\n Columns: {{ displayedColumns.join(', ') }} | Data received\n </div>\n </div>\n }\n </div>\n\n </div>\n\n</div>\n", styles: [".btn{min-width:120px}.mat-mdc-row .mat-mdc-cell{border-bottom:1px solid transparent;border-top:1px solid transparent;cursor:pointer}.mat-mdc-row:hover .mat-mdc-cell{border-color:currentColor;background-color:#f5f5f5}.container{height:400px;overflow:auto}.box{padding:10px;border:1px solid #ccc}\n"], dependencies: [{ kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2$1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i2$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i2$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: i2$1.FormGroupName, selector: "[formGroupName]", inputs: ["formGroupName"] }, { kind: "directive", type: i2$1.FormArrayName, selector: "[formArrayName]", inputs: ["formArrayName"] }, { kind: "component", type: i3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4.MatLabel, selector: "mat-label" }, { kind: "directive", type: i4.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "component", type: i5.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i6.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: i7.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i7.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i7.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "component", type: i8.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i9.MatTable, selector: "mat-table, table[mat-table]", exportAs: ["matTable"] }, { kind: "directive", type: i9.MatHeaderCellDef, selector: "[matHeaderCellDef]" }, { kind: "directive", type: i9.MatHeaderRowDef, selector: "[matHeaderRowDef]", inputs: ["matHeaderRowDef", "matHeaderRowDefSticky"] }, { kind: "directive", type: i9.MatColumnDef, selector: "[matColumnDef]", inputs: ["matColumnDef"] }, { kind: "directive", type: i9.MatCellDef, selector: "[matCellDef]" }, { kind: "directive", type: i9.MatRowDef, selector: "[matRowDef]", inputs: ["matRowDefColumns", "matRowDefWhen"] }, { kind: "directive", type: i9.MatHeaderCell, selector: "mat-header-cell, th[mat-header-cell]" }, { kind: "directive", type: i9.MatCell, selector: "mat-cell, td[mat-cell]" }, { kind: "component", type: i9.MatHeaderRow, selector: "mat-header-row, tr[mat-header-row]", exportAs: ["matHeaderRow"] }, { kind: "component", type: i9.MatRow, selector: "mat-row, tr[mat-row]", exportAs: ["matRow"] }, { kind: "component", type: i8$1.MatProgressBar, selector: "mat-progress-bar", inputs: ["color", "value", "bufferValue", "mode"], outputs: ["animationEnd"], exportAs: ["matProgressBar"] }, { kind: "component", type: i11.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "component", type: i12.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "directive", type: i13.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly"], exportAs: ["matInput"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "pipe", type: i1$1.JsonPipe, name: "json" }, { kind: "pipe", type: i1$1.TitleCasePipe, name: "titlecase" }] }); }
|
|
10797
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: RequestManagerStateDemoComponent, selector: "app-request-manager-state-demo", inputs: { server: "server", adapter: "adapter", mapper: "mapper" }, providers: [StateManagerDemoService, HTTPManagerService], viewQueries: [{ propertyName: "failedState", first: true, predicate: ["failedState"], descendants: true, static: true }, { propertyName: "pollingState", first: true, predicate: ["pollingState"], descendants: true, static: true }, { propertyName: "DBState", first: true, predicate: ["DBState"], descendants: true, static: true }], ngImport: i0, template: "<div style=\"margin: 2rem;\">\n\n <h2>\n HTTP Request State Manager\n </h2>\n\n <div [formGroup]=\"requestForm\" style=\"margin-top: 2rem;\">\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\">\n <mat-label>State Data Type</mat-label>\n <mat-select formControlName=\"datatype\">\n <mat-option value=\"ARRAY\">Array</mat-option>\n <mat-option value=\"OBJECT\">Object</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Adapter (Model)</mat-label>\n <mat-select formControlName=\"adapter\" #adapterSelect>\n <mat-option>None</mat-option>\n @for (adapter of sampleAdaptors; track adapter) {\n <mat-option [value]=\"adapter.value\">\n {{adapter.label}}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Mapper (Model)</mat-label>\n <mat-select formControlName=\"mapper\" #mapperSelect>\n <mat-option>None</mat-option>\n @for (mapper of sampleMappers; track mapper) {\n <mat-option [value]=\"mapper.value\">\n {{mapper.label}}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n\n @if (adapterSelect.value || mapperSelect.value) {\n <div style=\"display: flex; margin-bottom: 2rem;\">\n <div style=\"flex:1\" class=\"box\">\n <h3>Adapter (Incoming)</h3>\n @if (adapterSelect.value) {\n <div>\n {{ props(adapterSelect.value) | json }}\n </div>\n } @else {\n No Transformation\n }\n </div>\n <div style=\"flex:1\" class=\"box\">\n <h3>Mapper (Outgoing)</h3>\n @if (mapperSelect.value) {\n <div>\n {{ props(mapperSelect.value) | json }}\n </div>\n } @else {\n No Transformation\n }\n </div>\n </div>\n }\n\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\" style=\"flex:1\">\n <mat-label>RestPath (/ delimited)</mat-label>\n <input matInput placeholder=\"clients/list\" formControlName=\"path\">\n </mat-form-field>\n </div>\n <div>\n <div formArrayName=\"headers\">\n @for (task of headers.controls; track task; let i = $index) {\n <div [formGroupName]=\"i\">\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Key</mat-label>\n <input matInput placeholder=\"authentication\" formControlName=\"key\">\n </mat-form-field>\n <mat-form-field appearance=\"outline\" style=\"flex:1\">\n <mat-label>Value</mat-label>\n <input matInput placeholder=\"sample\" formControlName=\"value\">\n </mat-form-field>\n <div style=\"margin-top: .5rem;\">\n <button mat-icon-button (click)=\"removeHeader(i)\">\n <mat-icon>close</mat-icon>\n </button>\n </div>\n </div>\n </div>\n }\n </div>\n <button mat-stroked-button (click)=\"addHeader()\">Add Header</button>\n </div>\n <div style=\"margin-top: 2rem; display: flex; flex-direction:column; gap: 1rem;\">\n <div>\n <mat-slide-toggle #failedState>Retry on Failed</mat-slide-toggle>\n @if (failedState.checked) {\n <div style=\"display: flex; gap: .5rem; margin-top: 1rem;\" formGroupName=\"retry\">\n <mat-form-field appearance=\"outline\">\n <mat-label>#of Times</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"times\" value=\"3\">\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Delay Until Next</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"delay\" value=\"3\">\n </mat-form-field>\n </div>\n }\n </div>\n <div>\n <mat-slide-toggle #pollingState>Polling</mat-slide-toggle>\n @if (pollingState.checked) {\n <div>\n <mat-form-field appearance=\"outline\" style=\"margin-top: 1rem\">\n <mat-label>#of Seconds</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"polling\" value=\"3\">\n </mat-form-field>\n </div>\n }\n </div>\n <div>\n <mat-slide-toggle #DBState>Database Storage</mat-slide-toggle>\n @if (DBState.checked) {\n <div style=\"margin-top: 1rem\" formGroupName=\"database\">\n <div style=\"display: flex; gap: .5rem;\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Table Name</mat-label>\n <input matInput placeholder=\"table\" formControlName=\"table\" value=\"\">\n </mat-form-field>\n <div>\n <mat-form-field appearance=\"outline\">\n <mat-label>Expires In</mat-label>\n <mat-select formControlName=\"expiresIn\">\n <mat-option value=\"1m\">One Minute</mat-option>\n <mat-option value=\"1h\">One Hour</mat-option>\n <mat-option value=\"1d\">One Day</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n </div>\n <div style=\"display: flex; gap: .5rem;\">\n <mat-form-field appearance=\"outline\" style=\"flex:1\">\n <mat-label>Ignore Params (comma delimited)</mat-label>\n <input matInput placeholder=\"sort,active\" formControlName=\"ignoreQueryParams\" value=\"\">\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Watch Param Expiry</mat-label>\n <mat-select formControlName=\"queryParamsExpiresIn\">\n <mat-option value=\"never\">Never</mat-option>\n <mat-option value=\"10s\">10 Seconds</mat-option>\n <mat-option value=\"30s\">30 Seconds</mat-option>\n <mat-option value=\"1m\">1 Minute</mat-option>\n <mat-option value=\"5m\">5 Minutes</mat-option>\n <mat-option value=\"10m\">10 Minutes</mat-option>\n <mat-option value=\"1h\">1 Hour</mat-option>\n <mat-option value=\"1d\">1 Day</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n <div style=\"margin-top: .5rem; display: flex; gap: .5rem;\">\n <button mat-stroked-button color=\"warn\" (click)=\"onClearRecords()\">Clear Table</button>\n <button mat-stroked-button color=\"warn\" (click)=\"deleteDatabase()\">Delete Database</button>\n </div>\n </div>\n }\n </div>\n <div style=\"margin-top: 1rem; display: flex;\">\n <span style=\"flex:1\"></span>\n <button mat-stroked-button (click)=\"onSetStateOptions()\" [disabled]=\"!hasChanged\">\n Set API Request Options\n </button>\n </div>\n <div>\n @if ((error$ | async); as error) {\n <mat-error>\n {{ error }}\n </mat-error>\n }\n </div>\n </div>\n </div>\n\n <div style=\"margin-bottom: 1rem; margin-top: 2rem;\">\n @if ((isPending$ | async)) {\n <mat-progress-bar mode=\"indeterminate\"\n ></mat-progress-bar>\n }\n @if (pollingState.checked) {\n <div>\n @if (!(isPending$ | async) && (this.countdown$ | async) || -1 > 0) {\n <mat-progress-bar mode=\"determinate\"\n [value]=\"(this.countdown$ | async)\"\n ></mat-progress-bar>\n }\n </div>\n }\n\n </div>\n\n @if ((GET$ | async); as data) {\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1;\">CURRENT STATE ({{ dataType }})</h2>\n </div>\n @if (data.length > 1) {\n <div>\n <mat-form-field appearance=\"outline\">\n <mat-label>Records</mat-label>\n <mat-select [formControl]=\"selectedRecord\">\n <mat-option [value]=\"\">None</mat-option>\n @for (item of data; track $index) {\n <mat-option [value]=\"item\">\n {{ getRecordLabel(item) | titlecase }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n }\n @if ((dataObservable$ | async); as dataRecord) {\n <div>\n @if ((selectedRecord$ | async); as record) {\n <div>\n {{ record | json }}\n </div>\n } @else {\n No Record Selected from State\n }\n </div>\n }\n <div style=\"margin-top: 1rem;\">\n @if (data !== null && data?.length > 0) {\n <span>\n State contains a Total of {{ data.length }} Records\n </span>\n } @else {\n No Records\n }\n <div style=\"display: flex; gap: .5rem; text-align: end; justify-content: flex-end;\">\n <button class=\"btn\" mat-stroked-button (click)=\"onClearRecords()\">Clear</button>\n </div>\n </div>\n </div>\n }\n\n\n\n\n <div style=\"margin-top: 1rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">GET Request ({{ dataType }})</h2>\n <div>\n <button mat-raised-button (click)=\"onGetRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((GET_error$ | async); as get_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ get_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(GET$ | async) | jsonv\"></div> -->\n @if ((GET$ | async); as getData) {\n <div>{{ getData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">POST Request</h2>\n <div>\n <button mat-raised-button (click)=\"onCreateRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((POST_error$ | async); as post_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ post_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(POST$ | async) | jsonv\"></div> -->\n @if ((POST$ | async); as postData) {\n <div>{{ postData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">PUT Request</h2>\n <div>\n <button mat-raised-button (click)=\"onUpdateRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((PUT_error$ | async); as put_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ put_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(PUT$ | async) | jsonv\"></div> -->\n @if ((PUT$ | async); as putData) {\n <div>{{ putData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">DELETE Request</h2>\n <div>\n <button mat-raised-button (click)=\"onDeleteRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((DELETE_error$ | async); as delete_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ delete_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(DELETE$ | async) | jsonv\"></div> -->\n @if ((DELETE$ | async); as deleteData) {\n <div>{{ deleteData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">Streaming GET Request</h2>\n <div style=\"display: flex; gap: 1rem; align-items: center;\">\n <div style=\"display: flex; gap: 1rem; align-items: center;\">\n {{ streamType }}\n <button mat-icon-button [matMenuTriggerFor]=\"menu\">\n <mat-icon>data_usage</mat-icon>\n </button>\n </div>\n <mat-menu #menu=\"matMenu\">\n <button mat-menu-item *ngFor=\"let item of streamTypes\" (click)=\"onStreamType(item.id)\">{{ item.value }}</button>\n </mat-menu>\n <button mat-raised-button (click)=\"onStreamRequest()\" class=\"btn\" [disabled]=\"hasChanged\">Request</button>\n </div>\n </div>\n\n @if ((STREAM_error$ | async); as stream_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ stream_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n @if ((STREAM$ | async); as data) {\n <div class=\"container\">\n <table mat-table [dataSource]=\"data\" class=\"mat-elevation-z8\">\n\n <!-- Dynamic columns -->\n <ng-container *ngFor=\"let column of displayedColumns\" [matColumnDef]=\"column\">\n <th mat-header-cell *matHeaderCellDef> {{ column | titlecase }} </th>\n <td mat-cell *matCellDef=\"let element\">\n @if (isObject(element[column]); as objValue) {\n <pre style=\"margin: 0; font-size: 0.8em; white-space: pre-wrap;\">{{ objValue | json }}</pre>\n } @else {\n {{ element[column] }}\n }\n </td>\n </ng-container>\n\n <tr mat-header-row *matHeaderRowDef=\"displayedColumns\"></tr>\n <tr mat-row *matRowDef=\"let row; columns: displayedColumns;\"></tr>\n </table>\n\n <!-- Debug info -->\n <div style=\"margin-top: 1rem; font-size: 0.8em; color: #666;\">\n Columns: {{ displayedColumns.join(', ') }} | Data received\n </div>\n </div>\n }\n </div>\n\n </div>\n\n</div>\n", styles: [".btn{min-width:120px}.mat-mdc-row .mat-mdc-cell{border-bottom:1px solid transparent;border-top:1px solid transparent;cursor:pointer}.mat-mdc-row:hover .mat-mdc-cell{border-color:currentColor;background-color:#f5f5f5}.container{height:400px;overflow:auto}.box{padding:10px;border:1px solid #ccc}\n"], dependencies: [{ kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2$1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i2$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i2$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: i2$1.FormGroupName, selector: "[formGroupName]", inputs: ["formGroupName"] }, { kind: "directive", type: i2$1.FormArrayName, selector: "[formArrayName]", inputs: ["formArrayName"] }, { kind: "component", type: i3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4.MatLabel, selector: "mat-label" }, { kind: "directive", type: i4.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "component", type: i5.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i6.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: i7.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i7.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i7.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "component", type: i8.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i9.MatTable, selector: "mat-table, table[mat-table]", exportAs: ["matTable"] }, { kind: "directive", type: i9.MatHeaderCellDef, selector: "[matHeaderCellDef]" }, { kind: "directive", type: i9.MatHeaderRowDef, selector: "[matHeaderRowDef]", inputs: ["matHeaderRowDef", "matHeaderRowDefSticky"] }, { kind: "directive", type: i9.MatColumnDef, selector: "[matColumnDef]", inputs: ["matColumnDef"] }, { kind: "directive", type: i9.MatCellDef, selector: "[matCellDef]" }, { kind: "directive", type: i9.MatRowDef, selector: "[matRowDef]", inputs: ["matRowDefColumns", "matRowDefWhen"] }, { kind: "directive", type: i9.MatHeaderCell, selector: "mat-header-cell, th[mat-header-cell]" }, { kind: "directive", type: i9.MatCell, selector: "mat-cell, td[mat-cell]" }, { kind: "component", type: i9.MatHeaderRow, selector: "mat-header-row, tr[mat-header-row]", exportAs: ["matHeaderRow"] }, { kind: "component", type: i9.MatRow, selector: "mat-row, tr[mat-row]", exportAs: ["matRow"] }, { kind: "component", type: i8$1.MatProgressBar, selector: "mat-progress-bar", inputs: ["color", "value", "bufferValue", "mode"], outputs: ["animationEnd"], exportAs: ["matProgressBar"] }, { kind: "component", type: i11.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "component", type: i12.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "directive", type: i13.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly"], exportAs: ["matInput"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "pipe", type: i1$1.JsonPipe, name: "json" }, { kind: "pipe", type: i1$1.TitleCasePipe, name: "titlecase" }] }); }
|
|
10434
10798
|
}
|
|
10435
10799
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RequestManagerStateDemoComponent, decorators: [{
|
|
10436
10800
|
type: Component,
|
|
10437
|
-
args: [{ selector: 'app-request-manager-state-demo', providers: [StateManagerDemoService, HTTPManagerService], standalone: false, template: "<div style=\"margin: 2rem;\">\n\n <h2>\n HTTP Request State Manager\n </h2>\n\n <div [formGroup]=\"requestForm\" style=\"margin-top: 2rem;\">\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\">\n <mat-label>State Data Type</mat-label>\n <mat-select formControlName=\"datatype\">\n <mat-option value=\"ARRAY\">Array</mat-option>\n <mat-option value=\"OBJECT\">Object</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Adapter (Model)</mat-label>\n <mat-select formControlName=\"adapter\" #adapterSelect>\n <mat-option>None</mat-option>\n @for (adapter of sampleAdaptors; track adapter) {\n <mat-option [value]=\"adapter.value\">\n {{adapter.label}}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Mapper (Model)</mat-label>\n <mat-select formControlName=\"mapper\" #mapperSelect>\n <mat-option>None</mat-option>\n @for (mapper of sampleMappers; track mapper) {\n <mat-option [value]=\"mapper.value\">\n {{mapper.label}}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n\n @if (adapterSelect.value || mapperSelect.value) {\n <div style=\"display: flex; margin-bottom: 2rem;\">\n <div style=\"flex:1\" class=\"box\">\n <h3>Adapter (Incoming)</h3>\n @if (adapterSelect.value) {\n <div>\n {{ props(adapterSelect.value) | json }}\n </div>\n } @else {\n No Transformation\n }\n </div>\n <div style=\"flex:1\" class=\"box\">\n <h3>Mapper (Outgoing)</h3>\n @if (mapperSelect.value) {\n <div>\n {{ props(mapperSelect.value) | json }}\n </div>\n } @else {\n No Transformation\n }\n </div>\n </div>\n }\n\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\" style=\"flex:1\">\n <mat-label>RestPath (/ delimited)</mat-label>\n <input matInput placeholder=\"clients/list\" formControlName=\"path\">\n </mat-form-field>\n </div>\n <div>\n <div formArrayName=\"headers\">\n @for (task of headers.controls; track task; let i = $index) {\n <div [formGroupName]=\"i\">\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Key</mat-label>\n <input matInput placeholder=\"authentication\" formControlName=\"key\">\n </mat-form-field>\n <mat-form-field appearance=\"outline\" style=\"flex:1\">\n <mat-label>Value</mat-label>\n <input matInput placeholder=\"sample\" formControlName=\"value\">\n </mat-form-field>\n <div style=\"margin-top: .5rem;\">\n <button mat-icon-button (click)=\"removeHeader(i)\">\n <mat-icon>close</mat-icon>\n </button>\n </div>\n </div>\n </div>\n }\n </div>\n <button mat-stroked-button (click)=\"addHeader()\">Add Header</button>\n </div>\n <div style=\"margin-top: 2rem; display: flex; flex-direction:column; gap: 1rem;\">\n <div>\n <mat-slide-toggle #failedState>Retry on Failed</mat-slide-toggle>\n @if (failedState.checked) {\n <div style=\"display: flex; gap: .5rem; margin-top: 1rem;\" formGroupName=\"retry\">\n <mat-form-field appearance=\"outline\">\n <mat-label>#of Times</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"times\" value=\"3\">\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Delay Until Next</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"delay\" value=\"3\">\n </mat-form-field>\n </div>\n }\n </div>\n <div>\n <mat-slide-toggle #pollingState>Polling</mat-slide-toggle>\n @if (pollingState.checked) {\n <div>\n <mat-form-field appearance=\"outline\" style=\"margin-top: 1rem\">\n <mat-label>#of Seconds</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"polling\" value=\"3\">\n </mat-form-field>\n </div>\n }\n </div>\n <div>\n <mat-slide-toggle #DBState>Database Storage</mat-slide-toggle>\n @if (DBState.checked) {\n <div style=\"margin-top: 1rem\" formGroupName=\"database\">\n <div style=\"display: flex; gap: .5rem;\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Table Name</mat-label>\n <input matInput placeholder=\"table\" formControlName=\"table\" value=\"\">\n </mat-form-field>\n <div>\n <mat-form-field appearance=\"outline\">\n <mat-label>Expires In</mat-label>\n <mat-select formControlName=\"expiresIn\">\n <mat-option value=\"1m\">One Minute</mat-option>\n <mat-option value=\"1h\">One Hour</mat-option>\n <mat-option value=\"1d\">One Day</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n </div>\n <div style=\"display: flex; gap: .5rem;\">\n <mat-form-field appearance=\"outline\" style=\"flex:1\">\n <mat-label>Ignore Params (comma delimited)</mat-label>\n <input matInput placeholder=\"sort,active\" formControlName=\"ignoreQueryParams\" value=\"\">\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Watch Param Expiry</mat-label>\n <mat-select formControlName=\"queryParamsExpiresIn\">\n <mat-option value=\"never\">Never</mat-option>\n <mat-option value=\"10s\">10 Seconds</mat-option>\n <mat-option value=\"30s\">30 Seconds</mat-option>\n <mat-option value=\"1m\">1 Minute</mat-option>\n <mat-option value=\"5m\">5 Minutes</mat-option>\n <mat-option value=\"10m\">10 Minutes</mat-option>\n <mat-option value=\"1h\">1 Hour</mat-option>\n <mat-option value=\"1d\">1 Day</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n <div style=\"margin-top: .5rem; display: flex; gap: .5rem;\">\n <button mat-stroked-button color=\"warn\" (click)=\"onClearRecords()\">Clear Table</button>\n <button mat-stroked-button color=\"warn\" (click)=\"deleteDatabase()\">Delete Database</button>\n </div>\n </div>\n }\n </div>\n <div style=\"margin-top: 1rem; display: flex;\">\n <span style=\"flex:1\"></span>\n <button mat-stroked-button (click)=\"onSetStateOptions()\" [disabled]=\"!hasChanged\">\n Set API Request Options\n </button>\n </div>\n <div>\n @if ((error$ | async); as error) {\n <mat-error>\n {{ error }}\n </mat-error>\n }\n </div>\n </div>\n </div>\n\n <div style=\"margin-bottom: 1rem; margin-top: 2rem;\">\n @if ((isPending$ | async)) {\n <mat-progress-bar mode=\"indeterminate\"\n ></mat-progress-bar>\n }\n @if (pollingState.checked) {\n <div>\n @if (!(isPending$ | async) && (this.countdown$ | async) || -1 > 0) {\n <mat-progress-bar mode=\"determinate\"\n [value]=\"(this.countdown$ | async)\"\n ></mat-progress-bar>\n }\n </div>\n }\n\n </div>\n\n @if ((GET$ | async); as data) {\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1;\">CURRENT STATE ({{ dataType }})</h2>\n </div>\n @if (data.length > 1) {\n <div>\n <mat-form-field appearance=\"outline\">\n <mat-label>Records</mat-label>\n <mat-select [formControl]=\"selectedRecord\" [disabled]=\"data === null && data?.length === 0\">\n <mat-option [value]=\"\">None</mat-option>\n @for (item of data; track item) {\n <mat-option [value]=\"item\">\n {{ getRecordLabel(item) | titlecase }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n }\n @if ((dataObservable$ | async); as dataRecord) {\n <div>\n @if ((selectedRecord$ | async); as record) {\n <div>\n {{ record | json }}\n </div>\n } @else {\n No Record Selected from State\n }\n </div>\n }\n <div style=\"margin-top: 1rem;\">\n @if (data !== null && data?.length > 0) {\n <span>\n State contains a Total of {{ data.length }} Records\n </span>\n } @else {\n No Records\n }\n <div style=\"display: flex; gap: .5rem; text-align: end; justify-content: flex-end;\">\n <button class=\"btn\" mat-stroked-button (click)=\"onClearRecords()\">Clear</button>\n </div>\n </div>\n </div>\n }\n\n\n\n\n <div style=\"margin-top: 1rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">GET Request ({{ dataType }})</h2>\n <div>\n <button mat-raised-button (click)=\"onGetRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((GET_error$ | async); as get_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ get_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(GET$ | async) | jsonv\"></div> -->\n @if ((GET$ | async); as getData) {\n <div>{{ getData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">POST Request</h2>\n <div>\n <button mat-raised-button (click)=\"onCreateRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((POST_error$ | async); as post_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ post_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(POST$ | async) | jsonv\"></div> -->\n @if ((POST$ | async); as postData) {\n <div>{{ postData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">PUT Request</h2>\n <div>\n <button mat-raised-button (click)=\"onUpdateRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((PUT_error$ | async); as put_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ put_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(PUT$ | async) | jsonv\"></div> -->\n @if ((PUT$ | async); as putData) {\n <div>{{ putData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">DELETE Request</h2>\n <div>\n <button mat-raised-button (click)=\"onDeleteRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((DELETE_error$ | async); as delete_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ delete_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(DELETE$ | async) | jsonv\"></div> -->\n @if ((DELETE$ | async); as deleteData) {\n <div>{{ deleteData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">Streaming GET Request</h2>\n <div style=\"display: flex; gap: 1rem; align-items: center;\">\n <div style=\"display: flex; gap: 1rem; align-items: center;\">\n {{ streamType }}\n <button mat-icon-button [matMenuTriggerFor]=\"menu\">\n <mat-icon>data_usage</mat-icon>\n </button>\n </div>\n <mat-menu #menu=\"matMenu\">\n <button mat-menu-item *ngFor=\"let item of streamTypes\" (click)=\"onStreamType(item.id)\">{{ item.value }}</button>\n </mat-menu>\n <button mat-raised-button (click)=\"onStreamRequest()\" class=\"btn\" [disabled]=\"hasChanged\">Request</button>\n </div>\n </div>\n\n @if ((STREAM_error$ | async); as stream_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ stream_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n @if ((STREAM$ | async); as data) {\n <div class=\"container\">\n <table mat-table [dataSource]=\"data\" class=\"mat-elevation-z8\">\n\n <!-- Dynamic columns -->\n <ng-container *ngFor=\"let column of displayedColumns\" [matColumnDef]=\"column\">\n <th mat-header-cell *matHeaderCellDef> {{ column | titlecase }} </th>\n <td mat-cell *matCellDef=\"let element\">\n @if (isObject(element[column]); as objValue) {\n <pre style=\"margin: 0; font-size: 0.8em; white-space: pre-wrap;\">{{ objValue | json }}</pre>\n } @else {\n {{ element[column] }}\n }\n </td>\n </ng-container>\n\n <tr mat-header-row *matHeaderRowDef=\"displayedColumns\"></tr>\n <tr mat-row *matRowDef=\"let row; columns: displayedColumns;\"></tr>\n </table>\n\n <!-- Debug info -->\n <div style=\"margin-top: 1rem; font-size: 0.8em; color: #666;\">\n Columns: {{ displayedColumns.join(', ') }} | Data received\n </div>\n </div>\n }\n </div>\n\n </div>\n\n</div>\n", styles: [".btn{min-width:120px}.mat-mdc-row .mat-mdc-cell{border-bottom:1px solid transparent;border-top:1px solid transparent;cursor:pointer}.mat-mdc-row:hover .mat-mdc-cell{border-color:currentColor;background-color:#f5f5f5}.container{height:400px;overflow:auto}.box{padding:10px;border:1px solid #ccc}\n"] }]
|
|
10801
|
+
args: [{ selector: 'app-request-manager-state-demo', providers: [StateManagerDemoService, HTTPManagerService], standalone: false, template: "<div style=\"margin: 2rem;\">\n\n <h2>\n HTTP Request State Manager\n </h2>\n\n <div [formGroup]=\"requestForm\" style=\"margin-top: 2rem;\">\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\">\n <mat-label>State Data Type</mat-label>\n <mat-select formControlName=\"datatype\">\n <mat-option value=\"ARRAY\">Array</mat-option>\n <mat-option value=\"OBJECT\">Object</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Adapter (Model)</mat-label>\n <mat-select formControlName=\"adapter\" #adapterSelect>\n <mat-option>None</mat-option>\n @for (adapter of sampleAdaptors; track adapter) {\n <mat-option [value]=\"adapter.value\">\n {{adapter.label}}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Mapper (Model)</mat-label>\n <mat-select formControlName=\"mapper\" #mapperSelect>\n <mat-option>None</mat-option>\n @for (mapper of sampleMappers; track mapper) {\n <mat-option [value]=\"mapper.value\">\n {{mapper.label}}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n\n @if (adapterSelect.value || mapperSelect.value) {\n <div style=\"display: flex; margin-bottom: 2rem;\">\n <div style=\"flex:1\" class=\"box\">\n <h3>Adapter (Incoming)</h3>\n @if (adapterSelect.value) {\n <div>\n {{ props(adapterSelect.value) | json }}\n </div>\n } @else {\n No Transformation\n }\n </div>\n <div style=\"flex:1\" class=\"box\">\n <h3>Mapper (Outgoing)</h3>\n @if (mapperSelect.value) {\n <div>\n {{ props(mapperSelect.value) | json }}\n </div>\n } @else {\n No Transformation\n }\n </div>\n </div>\n }\n\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\" style=\"flex:1\">\n <mat-label>RestPath (/ delimited)</mat-label>\n <input matInput placeholder=\"clients/list\" formControlName=\"path\">\n </mat-form-field>\n </div>\n <div>\n <div formArrayName=\"headers\">\n @for (task of headers.controls; track task; let i = $index) {\n <div [formGroupName]=\"i\">\n <div style=\"display: flex; gap: .5rem\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Key</mat-label>\n <input matInput placeholder=\"authentication\" formControlName=\"key\">\n </mat-form-field>\n <mat-form-field appearance=\"outline\" style=\"flex:1\">\n <mat-label>Value</mat-label>\n <input matInput placeholder=\"sample\" formControlName=\"value\">\n </mat-form-field>\n <div style=\"margin-top: .5rem;\">\n <button mat-icon-button (click)=\"removeHeader(i)\">\n <mat-icon>close</mat-icon>\n </button>\n </div>\n </div>\n </div>\n }\n </div>\n <button mat-stroked-button (click)=\"addHeader()\">Add Header</button>\n </div>\n <div style=\"margin-top: 2rem; display: flex; flex-direction:column; gap: 1rem;\">\n <div>\n <mat-slide-toggle #failedState>Retry on Failed</mat-slide-toggle>\n @if (failedState.checked) {\n <div style=\"display: flex; gap: .5rem; margin-top: 1rem;\" formGroupName=\"retry\">\n <mat-form-field appearance=\"outline\">\n <mat-label>#of Times</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"times\" value=\"3\">\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Delay Until Next</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"delay\" value=\"3\">\n </mat-form-field>\n </div>\n }\n </div>\n <div>\n <mat-slide-toggle #pollingState>Polling</mat-slide-toggle>\n @if (pollingState.checked) {\n <div>\n <mat-form-field appearance=\"outline\" style=\"margin-top: 1rem\">\n <mat-label>#of Seconds</mat-label>\n <input matInput placeholder=\"3\" formControlName=\"polling\" value=\"3\">\n </mat-form-field>\n </div>\n }\n </div>\n <div>\n <mat-slide-toggle #DBState>Database Storage</mat-slide-toggle>\n @if (DBState.checked) {\n <div style=\"margin-top: 1rem\" formGroupName=\"database\">\n <div style=\"display: flex; gap: .5rem;\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Table Name</mat-label>\n <input matInput placeholder=\"table\" formControlName=\"table\" value=\"\">\n </mat-form-field>\n <div>\n <mat-form-field appearance=\"outline\">\n <mat-label>Expires In</mat-label>\n <mat-select formControlName=\"expiresIn\">\n <mat-option value=\"1m\">One Minute</mat-option>\n <mat-option value=\"1h\">One Hour</mat-option>\n <mat-option value=\"1d\">One Day</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n </div>\n <div style=\"display: flex; gap: .5rem;\">\n <mat-form-field appearance=\"outline\" style=\"flex:1\">\n <mat-label>Ignore Params (comma delimited)</mat-label>\n <input matInput placeholder=\"sort,active\" formControlName=\"ignoreQueryParams\" value=\"\">\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Watch Param Expiry</mat-label>\n <mat-select formControlName=\"queryParamsExpiresIn\">\n <mat-option value=\"never\">Never</mat-option>\n <mat-option value=\"10s\">10 Seconds</mat-option>\n <mat-option value=\"30s\">30 Seconds</mat-option>\n <mat-option value=\"1m\">1 Minute</mat-option>\n <mat-option value=\"5m\">5 Minutes</mat-option>\n <mat-option value=\"10m\">10 Minutes</mat-option>\n <mat-option value=\"1h\">1 Hour</mat-option>\n <mat-option value=\"1d\">1 Day</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n <div style=\"margin-top: .5rem; display: flex; gap: .5rem;\">\n <button mat-stroked-button color=\"warn\" (click)=\"onClearRecords()\">Clear Table</button>\n <button mat-stroked-button color=\"warn\" (click)=\"deleteDatabase()\">Delete Database</button>\n </div>\n </div>\n }\n </div>\n <div style=\"margin-top: 1rem; display: flex;\">\n <span style=\"flex:1\"></span>\n <button mat-stroked-button (click)=\"onSetStateOptions()\" [disabled]=\"!hasChanged\">\n Set API Request Options\n </button>\n </div>\n <div>\n @if ((error$ | async); as error) {\n <mat-error>\n {{ error }}\n </mat-error>\n }\n </div>\n </div>\n </div>\n\n <div style=\"margin-bottom: 1rem; margin-top: 2rem;\">\n @if ((isPending$ | async)) {\n <mat-progress-bar mode=\"indeterminate\"\n ></mat-progress-bar>\n }\n @if (pollingState.checked) {\n <div>\n @if (!(isPending$ | async) && (this.countdown$ | async) || -1 > 0) {\n <mat-progress-bar mode=\"determinate\"\n [value]=\"(this.countdown$ | async)\"\n ></mat-progress-bar>\n }\n </div>\n }\n\n </div>\n\n @if ((GET$ | async); as data) {\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1;\">CURRENT STATE ({{ dataType }})</h2>\n </div>\n @if (data.length > 1) {\n <div>\n <mat-form-field appearance=\"outline\">\n <mat-label>Records</mat-label>\n <mat-select [formControl]=\"selectedRecord\">\n <mat-option [value]=\"\">None</mat-option>\n @for (item of data; track $index) {\n <mat-option [value]=\"item\">\n {{ getRecordLabel(item) | titlecase }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n }\n @if ((dataObservable$ | async); as dataRecord) {\n <div>\n @if ((selectedRecord$ | async); as record) {\n <div>\n {{ record | json }}\n </div>\n } @else {\n No Record Selected from State\n }\n </div>\n }\n <div style=\"margin-top: 1rem;\">\n @if (data !== null && data?.length > 0) {\n <span>\n State contains a Total of {{ data.length }} Records\n </span>\n } @else {\n No Records\n }\n <div style=\"display: flex; gap: .5rem; text-align: end; justify-content: flex-end;\">\n <button class=\"btn\" mat-stroked-button (click)=\"onClearRecords()\">Clear</button>\n </div>\n </div>\n </div>\n }\n\n\n\n\n <div style=\"margin-top: 1rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">GET Request ({{ dataType }})</h2>\n <div>\n <button mat-raised-button (click)=\"onGetRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((GET_error$ | async); as get_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ get_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(GET$ | async) | jsonv\"></div> -->\n @if ((GET$ | async); as getData) {\n <div>{{ getData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">POST Request</h2>\n <div>\n <button mat-raised-button (click)=\"onCreateRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((POST_error$ | async); as post_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ post_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(POST$ | async) | jsonv\"></div> -->\n @if ((POST$ | async); as postData) {\n <div>{{ postData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">PUT Request</h2>\n <div>\n <button mat-raised-button (click)=\"onUpdateRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((PUT_error$ | async); as put_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ put_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(PUT$ | async) | jsonv\"></div> -->\n @if ((PUT$ | async); as putData) {\n <div>{{ putData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">DELETE Request</h2>\n <div>\n <button mat-raised-button (click)=\"onDeleteRequest()\" [disabled]=\"hasChanged\" class=\"btn\">Request</button>\n </div>\n </div>\n\n @if ((DELETE_error$ | async); as delete_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ delete_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n <!-- <div [innerHTML]=\"(DELETE$ | async) | jsonv\"></div> -->\n @if ((DELETE$ | async); as deleteData) {\n <div>{{ deleteData | json }}</div>\n }\n </div>\n\n </div>\n\n <div style=\"margin-top: 2rem\">\n <mat-divider></mat-divider>\n </div>\n\n <div style=\"margin-top: 2rem\">\n <div style=\"display: flex;\">\n <h2 style=\"flex:1\">Streaming GET Request</h2>\n <div style=\"display: flex; gap: 1rem; align-items: center;\">\n <div style=\"display: flex; gap: 1rem; align-items: center;\">\n {{ streamType }}\n <button mat-icon-button [matMenuTriggerFor]=\"menu\">\n <mat-icon>data_usage</mat-icon>\n </button>\n </div>\n <mat-menu #menu=\"matMenu\">\n <button mat-menu-item *ngFor=\"let item of streamTypes\" (click)=\"onStreamType(item.id)\">{{ item.value }}</button>\n </mat-menu>\n <button mat-raised-button (click)=\"onStreamRequest()\" class=\"btn\" [disabled]=\"hasChanged\">Request</button>\n </div>\n </div>\n\n @if ((STREAM_error$ | async); as stream_error) {\n <div style=\"margin-top: .5rem;\">\n <mat-error>{{ stream_error }}</mat-error>\n </div>\n }\n\n <div style=\"margin-top: 1rem;\">\n @if ((STREAM$ | async); as data) {\n <div class=\"container\">\n <table mat-table [dataSource]=\"data\" class=\"mat-elevation-z8\">\n\n <!-- Dynamic columns -->\n <ng-container *ngFor=\"let column of displayedColumns\" [matColumnDef]=\"column\">\n <th mat-header-cell *matHeaderCellDef> {{ column | titlecase }} </th>\n <td mat-cell *matCellDef=\"let element\">\n @if (isObject(element[column]); as objValue) {\n <pre style=\"margin: 0; font-size: 0.8em; white-space: pre-wrap;\">{{ objValue | json }}</pre>\n } @else {\n {{ element[column] }}\n }\n </td>\n </ng-container>\n\n <tr mat-header-row *matHeaderRowDef=\"displayedColumns\"></tr>\n <tr mat-row *matRowDef=\"let row; columns: displayedColumns;\"></tr>\n </table>\n\n <!-- Debug info -->\n <div style=\"margin-top: 1rem; font-size: 0.8em; color: #666;\">\n Columns: {{ displayedColumns.join(', ') }} | Data received\n </div>\n </div>\n }\n </div>\n\n </div>\n\n</div>\n", styles: [".btn{min-width:120px}.mat-mdc-row .mat-mdc-cell{border-bottom:1px solid transparent;border-top:1px solid transparent;cursor:pointer}.mat-mdc-row:hover .mat-mdc-cell{border-color:currentColor;background-color:#f5f5f5}.container{height:400px;overflow:auto}.box{padding:10px;border:1px solid #ccc}\n"] }]
|
|
10438
10802
|
}], ctorParameters: () => [], propDecorators: { server: [{
|
|
10439
10803
|
type: Input
|
|
10440
10804
|
}], adapter: [{
|
|
@@ -12165,11 +12529,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
12165
12529
|
class DatabaseDataDemoComponent {
|
|
12166
12530
|
constructor() {
|
|
12167
12531
|
this.db = inject(DatabaseManagerService);
|
|
12532
|
+
this.sql = inject(DexieSqlService);
|
|
12168
12533
|
this.destroy$ = new Subject();
|
|
12169
12534
|
this.dataToDisplay = [];
|
|
12170
12535
|
this.dataSource = new DatabaseDataSource(this.dataToDisplay);
|
|
12171
12536
|
this.displayedColumns = ['id', 'last_name', 'age', 'amount'];
|
|
12172
12537
|
this.names = ['Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia', 'Miller', 'Davis'];
|
|
12538
|
+
// ── SQL Query section ────────────────────────────────────────────────────
|
|
12539
|
+
this.sqlQuery = 'SELECT * FROM sample_table';
|
|
12540
|
+
this.sqlResults = [];
|
|
12541
|
+
this.sqlResultCols = [];
|
|
12542
|
+
this.sqlError = null;
|
|
12543
|
+
this.sqlLoading = false;
|
|
12173
12544
|
}
|
|
12174
12545
|
ngOnDestroy() {
|
|
12175
12546
|
this.destroy$.next();
|
|
@@ -12282,12 +12653,32 @@ class DatabaseDataDemoComponent {
|
|
|
12282
12653
|
return this.db.deleteTableRecord(table, id)
|
|
12283
12654
|
.pipe(tap((data) => console.log('deleteTableRecord:', data)));
|
|
12284
12655
|
}
|
|
12656
|
+
runSqlQuery() {
|
|
12657
|
+
const q = (this.sqlQuery ?? '').trim();
|
|
12658
|
+
if (!q)
|
|
12659
|
+
return;
|
|
12660
|
+
this.sqlLoading = true;
|
|
12661
|
+
this.sqlError = null;
|
|
12662
|
+
this.sqlResults = [];
|
|
12663
|
+
this.sqlResultCols = [];
|
|
12664
|
+
this.sql.query(q).subscribe({
|
|
12665
|
+
next: rows => {
|
|
12666
|
+
this.sqlResults = rows;
|
|
12667
|
+
this.sqlResultCols = rows.length > 0 ? Object.keys(rows[0]) : [];
|
|
12668
|
+
this.sqlLoading = false;
|
|
12669
|
+
},
|
|
12670
|
+
error: err => {
|
|
12671
|
+
this.sqlError = err?.message ?? String(err);
|
|
12672
|
+
this.sqlLoading = false;
|
|
12673
|
+
}
|
|
12674
|
+
});
|
|
12675
|
+
}
|
|
12285
12676
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DatabaseDataDemoComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
12286
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: DatabaseDataDemoComponent, selector: "app-database-data-demo", ngImport: i0, template: "<div style=\"padding: 2rem;\">\n\n <h2>\n Database Manager Service Demo\n </h2>\n\n <div style=\"margin-bottom: 1rem; display: flex; gap: 1rem;\">\n <button mat-stroked-button\n color=\"primary\"\n (click)=\"clearAllData()\"\n >\n Clear Data\n </button>\n <button mat-stroked-button\n color=\"primary\"\n (click)=\"updateData()\"\n >\n Update Data\n </button>\n <div style=\"flex:1\"></div>\n <button\n mat-stroked-button\n color=\"warn\"\n [disabled]=\"!dataToDisplay.length\"\n (click)=\"removeData()\"\n >\n Remove Data\n </button>\n <button mat-stroked-button\n color=\"primary\"\n (click)=\"addData()\"\n >\n Add Data\n </button>\n </div>\n\n <div class=\"table-container\">\n <table mat-table [dataSource]=\"dataSource\">\n\n <ng-container matColumnDef=\"id\">\n <th mat-header-cell *matHeaderCellDef>ID</th>\n <td mat-cell *matCellDef=\"let element\">{{element.id}}</td>\n </ng-container>\n\n <ng-container matColumnDef=\"last_name\">\n <th mat-header-cell *matHeaderCellDef>Last Name</th>\n <td mat-cell *matCellDef=\"let element\">{{element.last_name}}</td>\n </ng-container>\n\n <ng-container matColumnDef=\"age\">\n <th mat-header-cell *matHeaderCellDef>Age</th>\n <td mat-cell *matCellDef=\"let element\">{{element.age}}</td>\n </ng-container>\n\n <ng-container matColumnDef=\"amount\">\n <th mat-header-cell *matHeaderCellDef>Amount</th>\n <td mat-cell *matCellDef=\"let element\">{{element.amount | currency}}</td>\n </ng-container>\n\n <tr mat-header-row *matHeaderRowDef=\"displayedColumns\"></tr>\n <tr mat-row *matRowDef=\"let row; columns: displayedColumns;\"></tr>\n </table>\n </div>\n\n</div>\n", styles: [""], dependencies: [{ kind: "component", type: i3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i9.MatTable, selector: "mat-table, table[mat-table]", exportAs: ["matTable"] }, { kind: "directive", type: i9.MatHeaderCellDef, selector: "[matHeaderCellDef]" }, { kind: "directive", type: i9.MatHeaderRowDef, selector: "[matHeaderRowDef]", inputs: ["matHeaderRowDef", "matHeaderRowDefSticky"] }, { kind: "directive", type: i9.MatColumnDef, selector: "[matColumnDef]", inputs: ["matColumnDef"] }, { kind: "directive", type: i9.MatCellDef, selector: "[matCellDef]" }, { kind: "directive", type: i9.MatRowDef, selector: "[matRowDef]", inputs: ["matRowDefColumns", "matRowDefWhen"] }, { kind: "directive", type: i9.MatHeaderCell, selector: "mat-header-cell, th[mat-header-cell]" }, { kind: "directive", type: i9.MatCell, selector: "mat-cell, td[mat-cell]" }, { kind: "component", type: i9.MatHeaderRow, selector: "mat-header-row, tr[mat-header-row]", exportAs: ["matHeaderRow"] }, { kind: "component", type: i9.MatRow, selector: "mat-row, tr[mat-row]", exportAs: ["matRow"] }, { kind: "pipe", type: i1$1.CurrencyPipe, name: "currency" }] }); }
|
|
12677
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: DatabaseDataDemoComponent, selector: "app-database-data-demo", ngImport: i0, template: "<div style=\"padding: 2rem;\">\n\n <h2>\n Database Manager Service Demo\n </h2>\n\n <div style=\"margin-bottom: 1rem; display: flex; gap: 1rem;\">\n <button mat-stroked-button\n color=\"primary\"\n (click)=\"clearAllData()\"\n >\n Clear Data\n </button>\n <button mat-stroked-button\n color=\"primary\"\n (click)=\"updateData()\"\n >\n Update Data\n </button>\n <div style=\"flex:1\"></div>\n <button\n mat-stroked-button\n color=\"warn\"\n [disabled]=\"!dataToDisplay.length\"\n (click)=\"removeData()\"\n >\n Remove Data\n </button>\n <button mat-stroked-button\n color=\"primary\"\n (click)=\"addData()\"\n >\n Add Data\n </button>\n </div>\n\n <div class=\"table-container\">\n <table mat-table [dataSource]=\"dataSource\">\n\n <ng-container matColumnDef=\"id\">\n <th mat-header-cell *matHeaderCellDef>ID</th>\n <td mat-cell *matCellDef=\"let element\">{{element.id}}</td>\n </ng-container>\n\n <ng-container matColumnDef=\"last_name\">\n <th mat-header-cell *matHeaderCellDef>Last Name</th>\n <td mat-cell *matCellDef=\"let element\">{{element.last_name}}</td>\n </ng-container>\n\n <ng-container matColumnDef=\"age\">\n <th mat-header-cell *matHeaderCellDef>Age</th>\n <td mat-cell *matCellDef=\"let element\">{{element.age}}</td>\n </ng-container>\n\n <ng-container matColumnDef=\"amount\">\n <th mat-header-cell *matHeaderCellDef>Amount</th>\n <td mat-cell *matCellDef=\"let element\">{{element.amount | currency}}</td>\n </ng-container>\n\n <tr mat-header-row *matHeaderRowDef=\"displayedColumns\"></tr>\n <tr mat-row *matRowDef=\"let row; columns: displayedColumns;\"></tr>\n </table>\n </div>\n\n <!-- \u2500\u2500 SQL Query Section \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <mat-divider style=\"margin: 2rem 0;\"></mat-divider>\n\n <h3 style=\"margin-bottom: 1rem;\">SQL Query with DexieJS</h3>\n\n <div style=\"display: flex; gap: 1rem; align-items: flex-start;\">\n <mat-form-field style=\"flex: 1;\" appearance=\"outline\">\n <mat-label>SQL Query</mat-label>\n <textarea\n matInput\n [(ngModel)]=\"sqlQuery\"\n rows=\"3\"\n placeholder=\"e.g. SELECT * FROM sample_table WHERE age > 20\"\n (keydown.control.enter)=\"runSqlQuery()\"\n ></textarea>\n <mat-hint>Ctrl+Enter to execute</mat-hint>\n </mat-form-field>\n\n <button\n mat-flat-button\n color=\"primary\"\n style=\"margin-top: 4px;\"\n [disabled]=\"sqlLoading || !sqlQuery.trim()\"\n (click)=\"runSqlQuery()\"\n >\n {{ sqlLoading ? 'Running\u2026' : 'Execute' }}\n </button>\n </div>\n\n <!-- Error -->\n <div *ngIf=\"sqlError\" style=\"color: #d32f2f; margin: 0.5rem 0 1rem; font-size: 0.875rem;\">\n <mat-icon style=\"vertical-align: middle; font-size: 16px; height: 16px; width: 16px;\">error</mat-icon>\n {{ sqlError }}\n </div>\n\n <!-- Results table (dynamic columns) -->\n <div *ngIf=\"!sqlLoading && sqlResults.length > 0\" class=\"table-container\" style=\"margin-top: 1rem;\">\n <div>\n {{ sqlResults | json }}\n </div>\n <p style=\"font-size: 0.8rem; color: #666; margin-top: 0.5rem;\">\n {{ sqlResults.length }} row{{ sqlResults.length === 1 ? '' : 's' }} returned\n </p>\n </div>\n\n <!-- Empty state -->\n <p *ngIf=\"!sqlLoading && !sqlError && sqlResults.length === 0 && sqlQuery.trim()\"\n style=\"color: #888; font-size: 0.875rem; margin-top: 0.5rem;\">\n No results to display. Execute a query above.\n </p>\n\n</div>\n", styles: [""], dependencies: [{ kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4.MatLabel, selector: "mat-label" }, { kind: "directive", type: i4.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "component", type: i8.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i9.MatTable, selector: "mat-table, table[mat-table]", exportAs: ["matTable"] }, { kind: "directive", type: i9.MatHeaderCellDef, selector: "[matHeaderCellDef]" }, { kind: "directive", type: i9.MatHeaderRowDef, selector: "[matHeaderRowDef]", inputs: ["matHeaderRowDef", "matHeaderRowDefSticky"] }, { kind: "directive", type: i9.MatColumnDef, selector: "[matColumnDef]", inputs: ["matColumnDef"] }, { kind: "directive", type: i9.MatCellDef, selector: "[matCellDef]" }, { kind: "directive", type: i9.MatRowDef, selector: "[matRowDef]", inputs: ["matRowDefColumns", "matRowDefWhen"] }, { kind: "directive", type: i9.MatHeaderCell, selector: "mat-header-cell, th[mat-header-cell]" }, { kind: "directive", type: i9.MatCell, selector: "mat-cell, td[mat-cell]" }, { kind: "component", type: i9.MatHeaderRow, selector: "mat-header-row, tr[mat-header-row]", exportAs: ["matHeaderRow"] }, { kind: "component", type: i9.MatRow, selector: "mat-row, tr[mat-row]", exportAs: ["matRow"] }, { kind: "component", type: i12.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "directive", type: i13.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly"], exportAs: ["matInput"] }, { kind: "pipe", type: i1$1.JsonPipe, name: "json" }, { kind: "pipe", type: i1$1.CurrencyPipe, name: "currency" }] }); }
|
|
12287
12678
|
}
|
|
12288
12679
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DatabaseDataDemoComponent, decorators: [{
|
|
12289
12680
|
type: Component,
|
|
12290
|
-
args: [{ selector: 'app-database-data-demo', standalone: false, template: "<div style=\"padding: 2rem;\">\n\n <h2>\n Database Manager Service Demo\n </h2>\n\n <div style=\"margin-bottom: 1rem; display: flex; gap: 1rem;\">\n <button mat-stroked-button\n color=\"primary\"\n (click)=\"clearAllData()\"\n >\n Clear Data\n </button>\n <button mat-stroked-button\n color=\"primary\"\n (click)=\"updateData()\"\n >\n Update Data\n </button>\n <div style=\"flex:1\"></div>\n <button\n mat-stroked-button\n color=\"warn\"\n [disabled]=\"!dataToDisplay.length\"\n (click)=\"removeData()\"\n >\n Remove Data\n </button>\n <button mat-stroked-button\n color=\"primary\"\n (click)=\"addData()\"\n >\n Add Data\n </button>\n </div>\n\n <div class=\"table-container\">\n <table mat-table [dataSource]=\"dataSource\">\n\n <ng-container matColumnDef=\"id\">\n <th mat-header-cell *matHeaderCellDef>ID</th>\n <td mat-cell *matCellDef=\"let element\">{{element.id}}</td>\n </ng-container>\n\n <ng-container matColumnDef=\"last_name\">\n <th mat-header-cell *matHeaderCellDef>Last Name</th>\n <td mat-cell *matCellDef=\"let element\">{{element.last_name}}</td>\n </ng-container>\n\n <ng-container matColumnDef=\"age\">\n <th mat-header-cell *matHeaderCellDef>Age</th>\n <td mat-cell *matCellDef=\"let element\">{{element.age}}</td>\n </ng-container>\n\n <ng-container matColumnDef=\"amount\">\n <th mat-header-cell *matHeaderCellDef>Amount</th>\n <td mat-cell *matCellDef=\"let element\">{{element.amount | currency}}</td>\n </ng-container>\n\n <tr mat-header-row *matHeaderRowDef=\"displayedColumns\"></tr>\n <tr mat-row *matRowDef=\"let row; columns: displayedColumns;\"></tr>\n </table>\n </div>\n\n</div>\n" }]
|
|
12681
|
+
args: [{ selector: 'app-database-data-demo', standalone: false, template: "<div style=\"padding: 2rem;\">\n\n <h2>\n Database Manager Service Demo\n </h2>\n\n <div style=\"margin-bottom: 1rem; display: flex; gap: 1rem;\">\n <button mat-stroked-button\n color=\"primary\"\n (click)=\"clearAllData()\"\n >\n Clear Data\n </button>\n <button mat-stroked-button\n color=\"primary\"\n (click)=\"updateData()\"\n >\n Update Data\n </button>\n <div style=\"flex:1\"></div>\n <button\n mat-stroked-button\n color=\"warn\"\n [disabled]=\"!dataToDisplay.length\"\n (click)=\"removeData()\"\n >\n Remove Data\n </button>\n <button mat-stroked-button\n color=\"primary\"\n (click)=\"addData()\"\n >\n Add Data\n </button>\n </div>\n\n <div class=\"table-container\">\n <table mat-table [dataSource]=\"dataSource\">\n\n <ng-container matColumnDef=\"id\">\n <th mat-header-cell *matHeaderCellDef>ID</th>\n <td mat-cell *matCellDef=\"let element\">{{element.id}}</td>\n </ng-container>\n\n <ng-container matColumnDef=\"last_name\">\n <th mat-header-cell *matHeaderCellDef>Last Name</th>\n <td mat-cell *matCellDef=\"let element\">{{element.last_name}}</td>\n </ng-container>\n\n <ng-container matColumnDef=\"age\">\n <th mat-header-cell *matHeaderCellDef>Age</th>\n <td mat-cell *matCellDef=\"let element\">{{element.age}}</td>\n </ng-container>\n\n <ng-container matColumnDef=\"amount\">\n <th mat-header-cell *matHeaderCellDef>Amount</th>\n <td mat-cell *matCellDef=\"let element\">{{element.amount | currency}}</td>\n </ng-container>\n\n <tr mat-header-row *matHeaderRowDef=\"displayedColumns\"></tr>\n <tr mat-row *matRowDef=\"let row; columns: displayedColumns;\"></tr>\n </table>\n </div>\n\n <!-- \u2500\u2500 SQL Query Section \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <mat-divider style=\"margin: 2rem 0;\"></mat-divider>\n\n <h3 style=\"margin-bottom: 1rem;\">SQL Query with DexieJS</h3>\n\n <div style=\"display: flex; gap: 1rem; align-items: flex-start;\">\n <mat-form-field style=\"flex: 1;\" appearance=\"outline\">\n <mat-label>SQL Query</mat-label>\n <textarea\n matInput\n [(ngModel)]=\"sqlQuery\"\n rows=\"3\"\n placeholder=\"e.g. SELECT * FROM sample_table WHERE age > 20\"\n (keydown.control.enter)=\"runSqlQuery()\"\n ></textarea>\n <mat-hint>Ctrl+Enter to execute</mat-hint>\n </mat-form-field>\n\n <button\n mat-flat-button\n color=\"primary\"\n style=\"margin-top: 4px;\"\n [disabled]=\"sqlLoading || !sqlQuery.trim()\"\n (click)=\"runSqlQuery()\"\n >\n {{ sqlLoading ? 'Running\u2026' : 'Execute' }}\n </button>\n </div>\n\n <!-- Error -->\n <div *ngIf=\"sqlError\" style=\"color: #d32f2f; margin: 0.5rem 0 1rem; font-size: 0.875rem;\">\n <mat-icon style=\"vertical-align: middle; font-size: 16px; height: 16px; width: 16px;\">error</mat-icon>\n {{ sqlError }}\n </div>\n\n <!-- Results table (dynamic columns) -->\n <div *ngIf=\"!sqlLoading && sqlResults.length > 0\" class=\"table-container\" style=\"margin-top: 1rem;\">\n <div>\n {{ sqlResults | json }}\n </div>\n <p style=\"font-size: 0.8rem; color: #666; margin-top: 0.5rem;\">\n {{ sqlResults.length }} row{{ sqlResults.length === 1 ? '' : 's' }} returned\n </p>\n </div>\n\n <!-- Empty state -->\n <p *ngIf=\"!sqlLoading && !sqlError && sqlResults.length === 0 && sqlQuery.trim()\"\n style=\"color: #888; font-size: 0.875rem; margin-top: 0.5rem;\">\n No results to display. Execute a query above.\n </p>\n\n</div>\n" }]
|
|
12291
12682
|
}], ctorParameters: () => [] });
|
|
12292
12683
|
class DatabaseDataSource extends DataSource {
|
|
12293
12684
|
constructor(initialData) {
|
|
@@ -13293,5 +13684,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
13293
13684
|
* Generated bundle index. Do not edit.
|
|
13294
13685
|
*/
|
|
13295
13686
|
|
|
13296
|
-
export { ApiRequest, AppService, AsymmetricalEncryptionService, BatchOptions, BatchResult, CONFIG_SETTINGS_TOKEN, ChannelType, ConfigHTTPOptions, ConfigOptions, DataType, DatabaseDataDemoComponent, DatabaseManagerService, DatabaseStorage, DbService, ErrorDisplaySettings, GlobalStoreOptions, HTTPManagerService, HTTPManagerSignalsService, HTTPManagerStateService, HeadersService, HttpRequestManagerModule, HttpRequestServicesDemoComponent, InvalidFileInfoModel, LocalStorageDemoComponent, LocalStorageManagerService, LocalStorageOptions, LocalStorageSignalsDemoComponent, LocalStorageSignalsManagerService, LoggerService, NormalizedRequestOptionsModel, NotificationMessage, OperationResultModel, ParsingResultModel, PathQueryService, PathTrackerStateModel, PublicMessage, QueryParamsTrackerOptionsModel, QueryTrackerStateModel, Random, RandomHSLColor, RandomHexColor, RandomNumber, RandomNumbers, RandomNumbersUnique, RandomPaletteColor, RandomSignature, RandomStr, RandomVisibleColor, RequestErrorInterceptor, RequestHeadersInterceptor, RequestManagerDemoComponent, RequestManagerStateDemoComponent, RequestOptions, RequestService, RequestSignalsService, RetryOptions, SettingOptions, StateMessage, StateOperationResult, StateStorageOptions, StorageData, StorageOption, StorageType, StoreStateManagerService, StoreStateManagerSignalsService, StoreStateSignalsDemoComponent, StreamConfigModel, StreamEventMetadataModel, StreamEventModel, StreamOutputModel, StreamProgressModel, StreamType, SymmetricalEncryptionService, TableSchemaDef, UUID, UUID_STR, UploadDemoComponent, UploadValidationErrorModel, UserData, UtilsService, WSOptions, WebSocketMessageService, WithCredentialsInterceptor, calculateBatchProgress, countdown, createChannelName, delayedRetry, isErrorState, isPendingState, isSuccessState, requestPolling, requestStreaming, streamAI, streamAuto, streamEvents, streamJSON, streamNDJSON };
|
|
13687
|
+
export { ApiRequest, AppService, AsymmetricalEncryptionService, BatchOptions, BatchResult, CONFIG_SETTINGS_TOKEN, ChannelType, ConfigHTTPOptions, ConfigOptions, DataType, DatabaseDataDemoComponent, DatabaseManagerService, DatabaseStorage, DbService, DexieSqlService, ErrorDisplaySettings, ExecutionPlanType, GlobalStoreOptions, HTTPManagerService, HTTPManagerSignalsService, HTTPManagerStateService, HeadersService, HttpRequestManagerModule, HttpRequestServicesDemoComponent, InvalidFileInfoModel, LocalStorageDemoComponent, LocalStorageManagerService, LocalStorageOptions, LocalStorageSignalsDemoComponent, LocalStorageSignalsManagerService, LoggerService, NormalizedRequestOptionsModel, NotificationMessage, OperationResultModel, ParsingResultModel, PathQueryService, PathTrackerStateModel, PublicMessage, QueryParamsTrackerOptionsModel, QueryTrackerStateModel, Random, RandomHSLColor, RandomHexColor, RandomNumber, RandomNumbers, RandomNumbersUnique, RandomPaletteColor, RandomSignature, RandomStr, RandomVisibleColor, RequestErrorInterceptor, RequestHeadersInterceptor, RequestManagerDemoComponent, RequestManagerStateDemoComponent, RequestOptions, RequestService, RequestSignalsService, RetryOptions, SettingOptions, SqlParseError, SqlValidationError, StateMessage, StateOperationResult, StateStorageOptions, StorageData, StorageOption, StorageType, StoreStateManagerService, StoreStateManagerSignalsService, StoreStateSignalsDemoComponent, StreamConfigModel, StreamEventMetadataModel, StreamEventModel, StreamOutputModel, StreamProgressModel, StreamType, SymmetricalEncryptionService, TableSchemaDef, UUID, UUID_STR, UploadDemoComponent, UploadValidationErrorModel, UserData, UtilsService, WSOptions, WebSocketMessageService, WithCredentialsInterceptor, calculateBatchProgress, countdown, createChannelName, delayedRetry, isErrorState, isPendingState, isSuccessState, requestPolling, requestStreaming, streamAI, streamAuto, streamEvents, streamJSON, streamNDJSON };
|
|
13297
13688
|
//# sourceMappingURL=http-request-manager.mjs.map
|