@auxilium/datalynk-client 1.0.20 → 1.1.0-rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -15,6 +15,31 @@ function clean(obj, undefinedOnly = false) {
15
15
  }
16
16
  return obj;
17
17
  }
18
+ function deepCopy(value) {
19
+ try {
20
+ return structuredClone(value);
21
+ } catch {
22
+ return JSON.parse(JSONSanitize(value));
23
+ }
24
+ }
25
+ function dotNotation(obj, prop, set) {
26
+ if (obj == null || !prop) return void 0;
27
+ return prop.split(/[.[\]]/g).filter((prop2) => prop2.length).reduce((obj2, prop2, i, arr) => {
28
+ if (prop2[0] == '"' || prop2[0] == "'") prop2 = prop2.slice(1, -1);
29
+ if (!(obj2 == null ? void 0 : obj2.hasOwnProperty(prop2))) {
30
+ return void 0;
31
+ }
32
+ return obj2[prop2];
33
+ }, obj);
34
+ }
35
+ function isEqual(a, b) {
36
+ const ta = typeof a, tb = typeof b;
37
+ if (ta != "object" || a == null || (tb != "object" || b == null))
38
+ return ta == "function" && tb == "function" ? a.toString() == b.toString() : a === b;
39
+ const keys = Object.keys(a);
40
+ if (keys.length != Object.keys(b).length) return false;
41
+ return Object.keys(a).every((key) => isEqual(a[key], b[key]));
42
+ }
18
43
  function JSONAttemptParse(json) {
19
44
  try {
20
45
  return JSON.parse(json);
@@ -32,9 +57,214 @@ function JSONSanitize(obj, space) {
32
57
  return value;
33
58
  }, space);
34
59
  }
60
+ class ASet extends Array {
61
+ /** Number of elements in set */
62
+ get size() {
63
+ return this.length;
64
+ }
65
+ /**
66
+ * Array to create set from, duplicate values will be removed
67
+ * @param {T[]} elements Elements which will be added to set
68
+ */
69
+ constructor(elements = []) {
70
+ super();
71
+ if (!!(elements == null ? void 0 : elements["forEach"]))
72
+ elements.forEach((el) => this.add(el));
73
+ }
74
+ /**
75
+ * Add elements to set if unique
76
+ * @param items
77
+ */
78
+ add(...items) {
79
+ items.filter((el) => !this.has(el)).forEach((el) => this.push(el));
80
+ return this;
81
+ }
82
+ /**
83
+ * Remove all elements
84
+ */
85
+ clear() {
86
+ this.splice(0, this.length);
87
+ return this;
88
+ }
89
+ /**
90
+ * Delete elements from set
91
+ * @param items Elements that will be deleted
92
+ */
93
+ delete(...items) {
94
+ items.forEach((el) => {
95
+ const index = this.indexOf(el);
96
+ if (index != -1) this.splice(index, 1);
97
+ });
98
+ return this;
99
+ }
100
+ /**
101
+ * Create list of elements this set has which the comparison set does not
102
+ * @param {ASet<T>} set Set to compare against
103
+ * @return {ASet<T>} Different elements
104
+ */
105
+ difference(set) {
106
+ return new ASet(this.filter((el) => !set.has(el)));
107
+ }
108
+ /**
109
+ * Check if set includes element
110
+ * @param {T} el Element to look for
111
+ * @return {boolean} True if element was found, false otherwise
112
+ */
113
+ has(el) {
114
+ return this.indexOf(el) != -1;
115
+ }
116
+ /**
117
+ * Find index number of element, or -1 if it doesn't exist. Matches by equality not reference
118
+ *
119
+ * @param {T} search Element to find
120
+ * @param {number} fromIndex Starting index position
121
+ * @return {number} Element index number or -1 if missing
122
+ */
123
+ indexOf(search2, fromIndex) {
124
+ return super.findIndex((el) => isEqual(el, search2), fromIndex);
125
+ }
126
+ /**
127
+ * Create list of elements this set has in common with the comparison set
128
+ * @param {ASet<T>} set Set to compare against
129
+ * @return {boolean} Set of common elements
130
+ */
131
+ intersection(set) {
132
+ return new ASet(this.filter((el) => set.has(el)));
133
+ }
134
+ /**
135
+ * Check if this set has no elements in common with the comparison set
136
+ * @param {ASet<T>} set Set to compare against
137
+ * @return {boolean} True if nothing in common, false otherwise
138
+ */
139
+ isDisjointFrom(set) {
140
+ return this.intersection(set).size == 0;
141
+ }
142
+ /**
143
+ * Check if all elements in this set are included in the comparison set
144
+ * @param {ASet<T>} set Set to compare against
145
+ * @return {boolean} True if all elements are included, false otherwise
146
+ */
147
+ isSubsetOf(set) {
148
+ return this.findIndex((el) => !set.has(el)) == -1;
149
+ }
150
+ /**
151
+ * Check if all elements from comparison set are included in this set
152
+ * @param {ASet<T>} set Set to compare against
153
+ * @return {boolean} True if all elements are included, false otherwise
154
+ */
155
+ isSuperset(set) {
156
+ return set.findIndex((el) => !this.has(el)) == -1;
157
+ }
158
+ /**
159
+ * Create list of elements that are only in one set but not both (XOR)
160
+ * @param {ASet<T>} set Set to compare against
161
+ * @return {ASet<T>} New set of unique elements
162
+ */
163
+ symmetricDifference(set) {
164
+ return new ASet([...this.difference(set), ...set.difference(this)]);
165
+ }
166
+ /**
167
+ * Create joined list of elements included in this & the comparison set
168
+ * @param {ASet<T>} set Set join
169
+ * @return {ASet<T>} New set of both previous sets combined
170
+ */
171
+ union(set) {
172
+ return new ASet([...this, ...set]);
173
+ }
174
+ }
175
+ function sortByProp(prop, reverse = false) {
176
+ return function(a, b) {
177
+ const aVal = dotNotation(a, prop);
178
+ const bVal = dotNotation(b, prop);
179
+ if (typeof aVal == "number" && typeof bVal == "number")
180
+ return (reverse ? -1 : 1) * (aVal - bVal);
181
+ if (aVal > bVal) return reverse ? -1 : 1;
182
+ if (aVal < bVal) return reverse ? 1 : -1;
183
+ return 0;
184
+ };
185
+ }
35
186
  function makeArray(value) {
36
187
  return Array.isArray(value) ? value : [value];
37
188
  }
189
+ class Database {
190
+ constructor(database, tables, version2) {
191
+ __publicField2(this, "connection");
192
+ __publicField2(this, "tables");
193
+ this.database = database;
194
+ this.version = version2;
195
+ this.connection = new Promise((resolve, reject) => {
196
+ const req = indexedDB.open(this.database, this.version);
197
+ this.tables = tables.map((t) => {
198
+ t = typeof t == "object" ? t : { name: t };
199
+ return { ...t, name: t.name.toString() };
200
+ });
201
+ const tableNames = new ASet(this.tables.map((t) => t.name));
202
+ req.onerror = () => reject(req.error);
203
+ req.onsuccess = () => {
204
+ const db = req.result;
205
+ if (tableNames.symmetricDifference(new ASet(Array.from(db.objectStoreNames))).length) {
206
+ db.close();
207
+ Object.assign(this, new Database(this.database, this.tables, db.version + 1));
208
+ } else {
209
+ this.version = db.version;
210
+ resolve(db);
211
+ }
212
+ };
213
+ req.onupgradeneeded = () => {
214
+ const db = req.result;
215
+ const existingTables = new ASet(Array.from(db.objectStoreNames));
216
+ existingTables.difference(tableNames).forEach((name) => db.deleteObjectStore(name));
217
+ tableNames.difference(existingTables).forEach((name) => db.createObjectStore(name));
218
+ };
219
+ });
220
+ }
221
+ includes(name) {
222
+ return !!this.tables.find((t) => t.name == name.toString());
223
+ }
224
+ table(name) {
225
+ return new Table(this, name.toString());
226
+ }
227
+ }
228
+ class Table {
229
+ constructor(database, name) {
230
+ this.database = database;
231
+ this.name = name;
232
+ }
233
+ async tx(table, fn2, readonly = false) {
234
+ const db = await this.database.connection;
235
+ const tx = db.transaction(table, readonly ? "readonly" : "readwrite");
236
+ const store = tx.objectStore(table);
237
+ return new Promise((resolve, reject) => {
238
+ const request = fn2(store);
239
+ request.onsuccess = () => resolve(request.result);
240
+ request.onerror = () => reject(request.error);
241
+ });
242
+ }
243
+ add(value, key) {
244
+ return this.tx(this.name, (store) => store.add(value, key));
245
+ }
246
+ count() {
247
+ return this.tx(this.name, (store) => store.count(), true);
248
+ }
249
+ put(key, value) {
250
+ return this.tx(this.name, (store) => store.put(value, key));
251
+ }
252
+ getAll() {
253
+ return this.tx(this.name, (store) => store.getAll(), true);
254
+ }
255
+ getAllKeys() {
256
+ return this.tx(this.name, (store) => store.getAllKeys(), true);
257
+ }
258
+ get(key) {
259
+ return this.tx(this.name, (store) => store.get(key), true);
260
+ }
261
+ delete(key) {
262
+ return this.tx(this.name, (store) => store.delete(key));
263
+ }
264
+ clear() {
265
+ return this.tx(this.name, (store) => store.clear());
266
+ }
267
+ }
38
268
  function contrast(background) {
39
269
  const exploded = background == null ? void 0 : background.match(background.length >= 6 ? /[0-9a-fA-F]{2}/g : /[0-9a-fA-F]/g);
40
270
  if (!exploded || (exploded == null ? void 0 : exploded.length) < 3) return "black";
@@ -1486,6 +1716,33 @@ var BehaviorSubject = function(_super) {
1486
1716
  };
1487
1717
  return BehaviorSubject2;
1488
1718
  }(Subject);
1719
+ var EmptyError = createErrorClass(function(_super) {
1720
+ return function EmptyErrorImpl() {
1721
+ _super(this);
1722
+ this.name = "EmptyError";
1723
+ this.message = "no elements in sequence";
1724
+ };
1725
+ });
1726
+ function lastValueFrom(source, config2) {
1727
+ return new Promise(function(resolve, reject) {
1728
+ var _hasValue = false;
1729
+ var _value;
1730
+ source.subscribe({
1731
+ next: function(value) {
1732
+ _value = value;
1733
+ _hasValue = true;
1734
+ },
1735
+ error: reject,
1736
+ complete: function() {
1737
+ if (_hasValue) {
1738
+ resolve(_value);
1739
+ } else {
1740
+ reject(new EmptyError());
1741
+ }
1742
+ }
1743
+ });
1744
+ });
1745
+ }
1489
1746
  function filter(predicate, thisArg) {
1490
1747
  return operate(function(source, subscriber) {
1491
1748
  var index = 0;
@@ -1515,6 +1772,34 @@ function distinctUntilChanged(comparator, keySelector) {
1515
1772
  function defaultCompare(a, b) {
1516
1773
  return a === b;
1517
1774
  }
1775
+ function skip(count) {
1776
+ return filter(function(_, index) {
1777
+ return count <= index;
1778
+ });
1779
+ }
1780
+ function takeWhile(predicate, inclusive) {
1781
+ return operate(function(source, subscriber) {
1782
+ var index = 0;
1783
+ source.subscribe(createOperatorSubscriber(subscriber, function(value) {
1784
+ var result = predicate(value, index++);
1785
+ (result || inclusive) && subscriber.next(value);
1786
+ !result && subscriber.complete();
1787
+ }));
1788
+ });
1789
+ }
1790
+ async function getTheme(spoke, scope) {
1791
+ let theme = await fetch(`https://${spoke}.auxiliumgroup.com/api/js/auxilium/dijits/templates/login/${spoke}/theme.json`).then((res) => res.json()).catch(() => null);
1792
+ if (scope && theme != null && theme[scope]) theme = { ...theme, ...theme[scope] };
1793
+ return {
1794
+ found: !!theme,
1795
+ background: `https://${spoke}.auxiliumgroup.com/static/js/auxilium/dijits/templates/login/${spoke}/background.jpg`,
1796
+ color: "#c83232",
1797
+ logo: `https://${spoke}.auxiliumgroup.com/static/js/auxilium/dijits/templates/login/${spoke}/logo.png`,
1798
+ title: spoke.toUpperCase(),
1799
+ textColor: "white",
1800
+ ...theme || {}
1801
+ };
1802
+ }
1518
1803
  const _LoginPrompt = class _LoginPrompt {
1519
1804
  constructor(api, spoke, options = {}) {
1520
1805
  __publicField(this, "alert");
@@ -1523,7 +1808,6 @@ const _LoginPrompt = class _LoginPrompt {
1523
1808
  __publicField(this, "password");
1524
1809
  __publicField(this, "persist");
1525
1810
  __publicField(this, "username");
1526
- __publicField(this, "options");
1527
1811
  __publicField(this, "_done");
1528
1812
  /** Promise which resolves once login is complete */
1529
1813
  __publicField(this, "done", new Promise((res) => {
@@ -1531,13 +1815,10 @@ const _LoginPrompt = class _LoginPrompt {
1531
1815
  }));
1532
1816
  this.api = api;
1533
1817
  this.spoke = spoke;
1534
- this.options = {
1535
- title: this.spoke,
1536
- background: "#ffffff",
1537
- color: "#c83232",
1538
- textColor: "#000000",
1539
- ...clean(options, true)
1540
- };
1818
+ this.options = options;
1819
+ this.themeDefaults().then(() => this.render());
1820
+ }
1821
+ async render() {
1541
1822
  this.close();
1542
1823
  document.head.innerHTML += _LoginPrompt.css(this.options);
1543
1824
  const div = document.createElement("div");
@@ -1549,9 +1830,17 @@ const _LoginPrompt = class _LoginPrompt {
1549
1830
  this.password = document.querySelector('#datalynk-login-form input[name="password"]');
1550
1831
  this.persist = document.querySelector('#datalynk-login-form input[name="persist"]');
1551
1832
  this.username = document.querySelector('#datalynk-login-form input[name="username"]');
1552
- if (options.persist === false) this.persist.parentElement.remove();
1833
+ if (this.options.persist === false) this.persist.parentElement.remove();
1553
1834
  this.form.onsubmit = (event) => this.login(event);
1554
1835
  }
1836
+ async themeDefaults() {
1837
+ const theme = await getTheme(this.spoke, "login");
1838
+ this.options = {
1839
+ logoOnly: !this.options.title && !theme.found,
1840
+ ...theme,
1841
+ ...clean(this.options, true)
1842
+ };
1843
+ }
1555
1844
  /** Close the login prompt */
1556
1845
  close() {
1557
1846
  var _a, _b;
@@ -1597,11 +1886,11 @@ __publicField(_LoginPrompt, "css", (options) => `
1597
1886
  @import url('https://fonts.cdnfonts.com/css/ar-blanca');
1598
1887
 
1599
1888
  #datalynk-login {
1600
- --theme-background: ${options.background};
1889
+ --theme-background: ${options.backgroundColor ? options.backgroundColor : `url(${options.background})`};
1601
1890
  --theme-container: #000000cc;
1602
1891
  --theme-glow: ${options.glow || options.color};
1603
1892
  --theme-primary: ${options.color};
1604
- --theme-text: ${options.textColor};;
1893
+ --theme-text: ${options.textColor};
1605
1894
 
1606
1895
  position: fixed;
1607
1896
  left: 0;
@@ -1614,7 +1903,7 @@ __publicField(_LoginPrompt, "css", (options) => `
1614
1903
  font-family: sans-serif;
1615
1904
  z-index: 1000;
1616
1905
  }
1617
-
1906
+
1618
1907
  #datalynk-login .added-links {
1619
1908
  color: var(--theme-text);
1620
1909
  position: fixed;
@@ -1750,12 +2039,18 @@ __publicField(_LoginPrompt, "css", (options) => `
1750
2039
  /** Dynamically create HTML */
1751
2040
  __publicField(_LoginPrompt, "template", (options) => `
1752
2041
  <div id="datalynk-login">
2042
+ <!-- Used to check if image is valid -->
2043
+ ${!options.backgroundColor ? `
2044
+ <img src="${options.background}" onerror="document.querySelector('#datalynk-login').style.backgroundImage = 'url(https://datalynk.auxiliumgroup.com/static/js/auxilium/dijits/templates/login/datalynk/background.jpg)'" style="width: 0; height: 0;"/>
2045
+ ` : ""}
2046
+
1753
2047
  <div class="added-links">
1754
2048
  ${(options.addLinks || []).map((link) => `<a href="${link.url || "#"}" target="_blank">${link.text}</a>`).join(" | ")}
1755
2049
  </div>
1756
2050
  <div class="login-container">
1757
2051
  <div class="login-header">
1758
- ${options.title}
2052
+ ${options.logo ? `<img alt="Logo" src="${options.logo}" class="login-logo" style="height: 100px; width: auto;" onerror="this.style.display='none'" ${options.logoOnly ? `onload="document.querySelector('.login-title').remove()"` : ""}>` : ""}
2053
+ <span class="login-title" style="margin-left: 0.5rem">${options.title}</span>
1759
2054
  </div>
1760
2055
  <div class="login-content">
1761
2056
  <div class="login-body" style="max-width: 300px">
@@ -1801,13 +2096,17 @@ __publicField(_LoginPrompt, "template", (options) => `
1801
2096
  let LoginPrompt = _LoginPrompt;
1802
2097
  class Auth {
1803
2098
  constructor(api) {
2099
+ __publicField(this, "onlinePrompt");
1804
2100
  /** Current user as an observable */
1805
2101
  __publicField(this, "user$", new BehaviorSubject(void 0));
2102
+ var _a;
1806
2103
  this.api = api;
1807
2104
  this.api.token$.subscribe(async (token) => {
1808
2105
  if (token === void 0) return;
1809
2106
  this.user = await this.current(token);
1810
2107
  });
2108
+ if ((_a = this.api.options.offline) == null ? void 0 : _a.length)
2109
+ this.user$.pipe(filter((u) => u !== void 0)).subscribe((u) => localStorage.setItem("datalynk-user", JSON.stringify(u)));
1811
2110
  }
1812
2111
  /** Current user */
1813
2112
  get user() {
@@ -1830,7 +2129,9 @@ class Auth {
1830
2129
  var _a;
1831
2130
  if (!token) return null;
1832
2131
  else if (token == ((_a = this.user) == null ? void 0 : _a.token)) return this.user;
1833
- return this.api.request({ "$/auth/current": {} }, { token }).then((resp) => (resp == null ? void 0 : resp.login) ? resp : null);
2132
+ else if (typeof navigator != "undefined" && !navigator.onLine && typeof localStorage != "undefined" && localStorage.getItem("datalynk-user"))
2133
+ return JSON.parse(localStorage.getItem("datalynk-user"));
2134
+ return this.api.request([{ "$/auth/current": {} }, { "$/env/me": {} }], { token }).then((resp) => resp[0] || resp[1] ? { ...resp[0], ...resp[1] } : null);
1834
2135
  }
1835
2136
  /**
1836
2137
  * Automatically handle sessions by checking localStorage & URL parameters for a token & prompting
@@ -1841,17 +2142,20 @@ class Auth {
1841
2142
  * @return {Promise<void>} Login complete
1842
2143
  */
1843
2144
  async handleLogin(spoke, options) {
1844
- var _a;
2145
+ var _a, _b;
2146
+ if (this.onlinePrompt) window.removeEventListener("online", this.onlinePrompt);
2147
+ if ((_a = this.api.options.offline) == null ? void 0 : _a.length) window.removeEventListener("online", this.onlinePrompt = () => this.handleLogin(spoke, options));
1845
2148
  const urlToken = new URLSearchParams(location.search).get("datalynkToken");
1846
2149
  if (urlToken) {
1847
2150
  this.api.token = urlToken;
1848
2151
  location.href = location.href.replace(/datalynkToken=.*?(&|$)/gm, "");
1849
- } else if (this.api.token) {
1850
- if (((_a = this.api.jwtPayload) == null ? void 0 : _a.realm) != spoke) {
2152
+ } else if (this.api.token && !this.api.expired) {
2153
+ if (((_b = this.api.jwtPayload) == null ? void 0 : _b.realm) != spoke) {
1851
2154
  this.api.token = null;
1852
2155
  location.reload();
1853
2156
  }
1854
2157
  } else {
2158
+ this.api.token = null;
1855
2159
  await this.loginPrompt(spoke, options).done;
1856
2160
  location.reload();
1857
2161
  }
@@ -1956,6 +2260,7 @@ class Auth {
1956
2260
  * @return {Promise<{closed: string, new: string}>}
1957
2261
  */
1958
2262
  logout() {
2263
+ localStorage.removeItem("datalynk-user");
1959
2264
  return this.api.request({ "$/auth/logout": {} }).then((resp) => {
1960
2265
  this.api.token = null;
1961
2266
  return resp;
@@ -1970,11 +2275,13 @@ class Auth {
1970
2275
  * @return {Promise<any>} New session
1971
2276
  */
1972
2277
  reset(login, newPassword, code) {
1973
- return this.api.request({ "$/auth/mobile/rescue": {
1974
- user: login,
1975
- password: newPassword,
1976
- pin: code
1977
- } }).then((resp) => {
2278
+ return this.api.request({
2279
+ "$/auth/mobile/rescue": {
2280
+ user: login,
2281
+ password: newPassword,
2282
+ pin: code
2283
+ }
2284
+ }).then((resp) => {
1978
2285
  if (resp.token) this.api.token = resp.token;
1979
2286
  return resp;
1980
2287
  });
@@ -2033,7 +2340,7 @@ class Files {
2033
2340
  })).then(async (files2) => {
2034
2341
  if (associate) {
2035
2342
  let id = typeof associate.row == "number" ? associate.row : associate.row[associate.pk || "id"];
2036
- if (!id) id = await this.api.slice(associate.slice).insert(associate.row).id();
2343
+ if (!id) id = await this.api.slice(associate.slice).insert(associate.row).id().exec();
2037
2344
  await this.associate(files2.map((f2) => f2.id), associate == null ? void 0 : associate.slice, associate == null ? void 0 : associate.row, associate == null ? void 0 : associate.field);
2038
2345
  }
2039
2346
  return files2;
@@ -2193,23 +2500,13 @@ const Serializer = {
2193
2500
  }
2194
2501
  }
2195
2502
  };
2196
- const _Slice = class _Slice {
2197
- /**
2198
- * An object to aid in constructing requests
2199
- *
2200
- * @param {number} slice Slice ID to interact with
2201
- * @param {Api} api Api to send the requests through
2202
- */
2203
- constructor(slice, api) {
2503
+ class ApiCall {
2504
+ constructor(api, slice) {
2204
2505
  __publicField(this, "operation");
2205
2506
  __publicField(this, "popField");
2206
2507
  __publicField(this, "request", {});
2207
2508
  /** Log response automatically */
2208
2509
  __publicField(this, "debugging");
2209
- /** Unsubscribe from changes, undefined if not subscribed */
2210
- __publicField(this, "unsubscribe");
2211
- /** Cached slice data as an observable */
2212
- __publicField(this, "cache$", new BehaviorSubject([]));
2213
2510
  /**
2214
2511
  * Whitelist and alias fields. Alias of `fields()`
2215
2512
  * @example
@@ -2221,16 +2518,8 @@ const _Slice = class _Slice {
2221
2518
  * @return {Slice<T>}
2222
2519
  */
2223
2520
  __publicField(this, "alias", this.fields);
2224
- this.slice = slice;
2225
2521
  this.api = api;
2226
- }
2227
- /** Cached slice data */
2228
- get cache() {
2229
- return this.cache$.getValue();
2230
- }
2231
- /** Set cached data & alert subscribers */
2232
- set cache(cache) {
2233
- this.cache$.next(cache);
2522
+ this.slice = slice;
2234
2523
  }
2235
2524
  /** Get raw API request */
2236
2525
  get raw() {
@@ -2325,7 +2614,8 @@ const _Slice = class _Slice {
2325
2614
  */
2326
2615
  exec(options) {
2327
2616
  if (!this.operation) throw new Error("No operation chosen");
2328
- return this.api.request(this.raw, options).then((resp) => {
2617
+ const request = this.raw;
2618
+ return this.api.request(request, options).then((resp) => {
2329
2619
  if (this.debugging) console.log(resp);
2330
2620
  return resp;
2331
2621
  });
@@ -2442,9 +2732,9 @@ const _Slice = class _Slice {
2442
2732
  * @param {T} rows Rows to add to slice
2443
2733
  * @returns {this<T>}
2444
2734
  */
2445
- save(...rows) {
2735
+ save(rows) {
2446
2736
  this.operation = "$/slice/multisave";
2447
- this.request.rows = rows;
2737
+ this.request.rows = makeArray(rows);
2448
2738
  return this;
2449
2739
  }
2450
2740
  /**
@@ -2465,30 +2755,6 @@ const _Slice = class _Slice {
2465
2755
  }
2466
2756
  return this;
2467
2757
  }
2468
- /**
2469
- * Synchronize cache with server
2470
- * @example
2471
- * ```ts
2472
- * const slice: Slice = new Slice<T>(Slices.Contact);
2473
- * slice.sync().subscribe((rows: T[]) => {});
2474
- * ```
2475
- * @param {boolean} on Enable/disable events
2476
- * @return {BehaviorSubject<T[]>} Cache which can be subscribed to
2477
- */
2478
- sync(on = true) {
2479
- if (on) {
2480
- new _Slice(this.slice, this.api).select().rows().exec().then((rows) => this.cache = rows);
2481
- if (!this.unsubscribe) this.unsubscribe = this.api.socket.sliceEvents(this.slice, (event) => {
2482
- const ids = [...event.data.new, ...event.data.changed];
2483
- new _Slice(this.slice, this.api).select(ids).rows().exec().then((rows) => this.cache = [...this.cache.filter((c) => c.id != null && !ids.includes(c.id)), ...rows]);
2484
- this.cache = this.cache.filter((v) => v.id && !event.data.lost.includes(v.id));
2485
- });
2486
- return this.cache$;
2487
- } else if (this.unsubscribe) {
2488
- this.unsubscribe();
2489
- this.unsubscribe = null;
2490
- }
2491
- }
2492
2758
  /**
2493
2759
  * Set the request type to update
2494
2760
  * @example
@@ -2531,6 +2797,7 @@ const _Slice = class _Slice {
2531
2797
  Object.entries(field).forEach(([key, value2]) => this.where(key, "==", value2));
2532
2798
  } else {
2533
2799
  const w = raw ? field : [(() => {
2800
+ if (operator == null ? void 0 : operator.startsWith("$")) return operator;
2534
2801
  if (operator == "==") return "$eq";
2535
2802
  if (operator == "!=") return "$neq";
2536
2803
  if (operator == ">") return "$gt";
@@ -2556,9 +2823,270 @@ const _Slice = class _Slice {
2556
2823
  }
2557
2824
  return this;
2558
2825
  }
2559
- };
2560
- __publicField(_Slice, "api");
2561
- let Slice = _Slice;
2826
+ }
2827
+ class Slice {
2828
+ /**
2829
+ * An object to aid in constructing requests
2830
+ *
2831
+ * @param {number} slice Slice ID to interact with
2832
+ * @param {Api} api Api to send the requests through
2833
+ */
2834
+ constructor(slice, api) {
2835
+ __publicField(this, "table");
2836
+ __publicField(this, "info");
2837
+ __publicField(this, "pendingInsert", 0);
2838
+ /** Unsubscribe from changes, undefined if not subscribed */
2839
+ __publicField(this, "unsubscribe");
2840
+ /** Cached slice data as an observable */
2841
+ __publicField(this, "cache$", new BehaviorSubject([]));
2842
+ var _a;
2843
+ this.slice = slice;
2844
+ this.api = api;
2845
+ if (this.offlineEnabled) {
2846
+ this.table = (_a = api.database) == null ? void 0 : _a.table(slice.toString());
2847
+ this.table.getAll().then((resp) => this.cache = resp);
2848
+ this.cache$.pipe(skip(1)).subscribe(async (cache) => {
2849
+ var _a2;
2850
+ await ((_a2 = this.table) == null ? void 0 : _a2.clear());
2851
+ await Promise.all(cache.map((c) => {
2852
+ var _a3;
2853
+ return (_a3 = this.table) == null ? void 0 : _a3.put(c.id, c);
2854
+ }));
2855
+ this.fixIncrement();
2856
+ });
2857
+ this.sync();
2858
+ window.addEventListener("online", async () => {
2859
+ if (this.api.expired) await lastValueFrom(this.api.auth.user$.pipe(skip(1), takeWhile((u) => !u || this.api.expired, true)));
2860
+ this.pushChanges();
2861
+ });
2862
+ }
2863
+ }
2864
+ /** Cached slice data */
2865
+ get cache() {
2866
+ return this.cache$.getValue();
2867
+ }
2868
+ /** Set cached data & alert subscribers */
2869
+ set cache(cache) {
2870
+ this.cache$.next(cache);
2871
+ }
2872
+ /** Is slice offline support enabled */
2873
+ get offlineEnabled() {
2874
+ var _a;
2875
+ return (_a = this.api.database) == null ? void 0 : _a.includes(this.slice.toString());
2876
+ }
2877
+ fixIncrement() {
2878
+ var _a;
2879
+ this.pendingInsert = ((_a = this.cache.toSorted(sortByProp("id")).pop()) == null ? void 0 : _a["id"]) || 0;
2880
+ }
2881
+ execWrapper(call) {
2882
+ const onlineExec = call.exec.bind(call);
2883
+ return () => {
2884
+ if (this.offlineEnabled && navigator && !(navigator == null ? void 0 : navigator.onLine)) {
2885
+ const where = (row, condition) => {
2886
+ if (Array.isArray(condition) ? !condition.length : condition == null) return true;
2887
+ if (!Array.isArray(condition)) return condition;
2888
+ if (condition[0] == "$field") return row[condition[1]];
2889
+ if (condition[0] == "$not") return !where(row, condition.slice(1));
2890
+ if (condition[0] == "$and") return condition.slice(1).filter((v) => where(row, v)).length == condition.length - 1;
2891
+ if (condition[0] == "$or") return !!condition.slice(1).find((v) => where(row, v));
2892
+ if (condition[0] == "$eq") return where(row, condition[1]) == where(row, condition[2]);
2893
+ if (condition[0] == "$neq") return where(row, condition[1]) != where(row, condition[2]);
2894
+ if (condition[0] == "$gt") return where(row, condition[1]) > where(row, condition[2]);
2895
+ if (condition[0] == "$gte") return where(row, condition[1]) >= where(row, condition[2]);
2896
+ if (condition[0] == "$lt") return where(row, condition[1]) < where(row, condition[2]);
2897
+ if (condition[0] == "$lte") return where(row, condition[1]) <= where(row, condition[2]);
2898
+ if (condition[0] == "$mod") return where(row, condition[1]) % where(row, condition[2]);
2899
+ return condition[0];
2900
+ };
2901
+ let request = call.raw, resp = {};
2902
+ if (request["$/slice/delete"]) {
2903
+ const found = this.cache.filter((r) => where(r, request["$/slice/delete"]["where"])).map((r) => r.id);
2904
+ this.cache = this.cache.map((r) => found.includes(r.id) ? { ...r, _sync: "delete" } : r);
2905
+ resp = {
2906
+ affected: found.length,
2907
+ "ignored-keys": [],
2908
+ keys: found,
2909
+ tx: null
2910
+ };
2911
+ } else if (request["$/slice/report"]) {
2912
+ resp["rows"] = deepCopy(this.cache).filter((r) => (r == null ? void 0 : r._sync) != "delete").filter((r) => r && where(r, request["$/slice/report"]["where"]));
2913
+ if (request["order"]) resp["rows"] = resp["rows"].toSorted(sortByProp(request["order"][1][1], request["order"] == "$desc"));
2914
+ if (request["limit"]) resp["rows"] = resp["rows"].slice(0, request["limit"]);
2915
+ if (request["fields"]) {
2916
+ if (request["fields"]["$count"]) resp["rows"] = { count: resp["rows"].length };
2917
+ else resp["rows"] = resp["rows"].map((r) => Object.entries(request["fields"]).reduce((acc, [k, v]) => ({
2918
+ ...acc,
2919
+ [v]: r[k]
2920
+ }), {}));
2921
+ }
2922
+ } else if (request["$/slice/xinsert"]) {
2923
+ const rows = request["$/slice/xinsert"]["rows"].map((r) => ({
2924
+ ...r,
2925
+ id: -++this.pendingInsert,
2926
+ _sync: "insert"
2927
+ }));
2928
+ this.cache = [...this.cache, ...rows];
2929
+ resp = {
2930
+ failed: [],
2931
+ granted: rows.map((r) => Object.keys(r).reduce((acc, key) => ({ ...acc, [key]: true }), {})),
2932
+ "ignored-fields": [],
2933
+ keys: rows.map((r) => r.id),
2934
+ publish: 1,
2935
+ tx: null
2936
+ };
2937
+ } else if (request["$/slice/xupdate"]) {
2938
+ const ids = request["$/slice/xupdate"]["rows"].map((r) => r.id);
2939
+ this.cache = [
2940
+ ...this.cache.filter((c) => !ids.includes(c.id)),
2941
+ ...request["$/slice/xupdate"]["rows"].map((r) => ({ ...r, _sync: "update" }))
2942
+ ].toSorted(sortByProp("id"));
2943
+ resp = {
2944
+ failed: [],
2945
+ granted: request["$/slice/xupdate"]["rows"].map((r) => Object.keys(r).reduce((acc, key) => ({
2946
+ ...acc,
2947
+ [key]: true
2948
+ }), {})),
2949
+ "ignored-fields": [],
2950
+ keys: request["$/slice/xupdate"]["rows"].map((r) => r.id),
2951
+ publish: 1,
2952
+ tx: null
2953
+ };
2954
+ }
2955
+ if (request["$pop"]) {
2956
+ resp = request["$pop"].split(":").reduce((acc, key) => acc[JSONAttemptParse(key)], resp);
2957
+ }
2958
+ return Promise.resolve(resp);
2959
+ } else {
2960
+ return onlineExec();
2961
+ }
2962
+ };
2963
+ }
2964
+ async pushChanges() {
2965
+ if (this.offlineEnabled && navigator && navigator.onLine) {
2966
+ await Promise.allSettled(
2967
+ this.cache.values().map((value) => {
2968
+ if (value._sync == "delete") {
2969
+ return this.delete(value.id).exec();
2970
+ } else if (value._sync == "insert") {
2971
+ return this.insert({ ...value, id: void 0, _sync: void 0 }).exec();
2972
+ } else if (value._sync == "update") {
2973
+ return this.update({
2974
+ ...value,
2975
+ _sync: void 0
2976
+ }).where("_updatedDate", "==", value.modified).exec();
2977
+ }
2978
+ }).filter((r) => !!r)
2979
+ );
2980
+ }
2981
+ }
2982
+ /**
2983
+ * Get slice information
2984
+ * @param reload Ignore cache & reload info
2985
+ * @return {Promise<SliceInfo>}
2986
+ */
2987
+ async getInfo(reload) {
2988
+ var _a, _b, _c;
2989
+ const getType = (field) => {
2990
+ if (field.options) {
2991
+ let t = field.options.split("|").map((o) => `'${o}'`).join(" | ");
2992
+ if (field.type == "control_checkbox") t = `(${t})[]`;
2993
+ return t;
2994
+ }
2995
+ if (field.type == "control_checkbox" || field.type == "boolean") return "boolean";
2996
+ if (field.type == "control_new_date" || field.type == "control_new_time" || field.type == "control_new_datetime" || field.type == "Date")
2997
+ return "Date";
2998
+ if (field.type == "control_number" || field.type == "number") return "number";
2999
+ return "string";
3000
+ };
3001
+ if (this.info && !reload) return Promise.resolve(this.info);
3002
+ this.info = await this.api.request({ "$/slice/fetch": { slice: this.slice } });
3003
+ const fields = ((_c = (_b = (_a = this.info) == null ? void 0 : _a.meta) == null ? void 0 : _b.presentation) == null ? void 0 : _c.fields) ? Object.values(this.info.meta.presentation.fields) : [];
3004
+ this.info.types = fields.filter((value) => value.id != void 0).map((value) => {
3005
+ var _a2;
3006
+ return {
3007
+ key: value.id.toString(),
3008
+ options: (_a2 = value.options) == null ? void 0 : _a2.split("|"),
3009
+ readonly: !!value.readonly || value.id.startsWith("fid") || ["id", "creatorRef", "created", "modifierRef", "modified"].includes(value.id),
3010
+ required: !!value.required,
3011
+ type: getType(value)
3012
+ };
3013
+ });
3014
+ return this.info;
3015
+ }
3016
+ /**
3017
+ * Synchronize cache with server
3018
+ * @example
3019
+ * ```ts
3020
+ * const slice: Slice = new Slice<T>(Slices.Contact);
3021
+ * slice.sync().subscribe((rows: T[]) => {});
3022
+ * ```
3023
+ * @param {boolean} on Enable/disable events
3024
+ * @return {BehaviorSubject<T[]>} Cache which can be subscribed to
3025
+ */
3026
+ sync(on = true) {
3027
+ if (on) {
3028
+ this.pushChanges().then(() => this.select().rows().exec().then((rows) => this.cache = rows));
3029
+ if (!this.unsubscribe) this.unsubscribe = this.api.socket.sliceEvents(this.slice, (event) => {
3030
+ const ids = [...event.data.new, ...event.data.changed];
3031
+ this.select(ids).rows().exec().then((rows) => this.cache = [...this.cache.filter((c) => c.id != null && !ids.includes(c.id)), ...rows]);
3032
+ this.cache = this.cache.filter((v) => v.id && !event.data.lost.includes(v.id));
3033
+ });
3034
+ return this.cache$;
3035
+ } else if (this.unsubscribe) {
3036
+ this.unsubscribe();
3037
+ this.unsubscribe = null;
3038
+ }
3039
+ }
3040
+ // Transaction wrapper =============================================================================================
3041
+ /**
3042
+ * {@inheritDoc ApiCall.count}
3043
+ */
3044
+ count(arg = "id") {
3045
+ const call = new ApiCall(this.api, this.slice);
3046
+ call.exec = this.execWrapper(call);
3047
+ return call.count(arg);
3048
+ }
3049
+ /**
3050
+ * {@inheritDoc ApiCall.delete}
3051
+ */
3052
+ delete(id) {
3053
+ const call = new ApiCall(this.api, this.slice);
3054
+ call.exec = this.execWrapper(call);
3055
+ return call.delete(id);
3056
+ }
3057
+ /**
3058
+ * {@inheritDoc ApiCall.insert}
3059
+ */
3060
+ insert(rows) {
3061
+ const call = new ApiCall(this.api, this.slice);
3062
+ call.exec = this.execWrapper(call);
3063
+ return call.insert(rows);
3064
+ }
3065
+ /**
3066
+ * {@inheritDoc ApiCall.save}
3067
+ */
3068
+ save(rows) {
3069
+ const call = new ApiCall(this.api, this.slice);
3070
+ call.exec = this.execWrapper(call);
3071
+ return call.save(rows);
3072
+ }
3073
+ /**
3074
+ * {@inheritDoc ApiCall.select}
3075
+ */
3076
+ select(id) {
3077
+ const call = new ApiCall(this.api, this.slice);
3078
+ call.exec = this.execWrapper(call);
3079
+ return call.select(id);
3080
+ }
3081
+ /**
3082
+ * {@inheritDoc ApiCall.update}
3083
+ */
3084
+ update(rows) {
3085
+ const call = new ApiCall(this.api, this.slice);
3086
+ call.exec = this.execWrapper(call);
3087
+ return call.update(rows);
3088
+ }
3089
+ }
2562
3090
  class Socket {
2563
3091
  constructor(api, options = {}) {
2564
3092
  __publicField(this, "listeners", []);
@@ -2691,8 +3219,8 @@ class Superuser {
2691
3219
  } });
2692
3220
  }
2693
3221
  }
2694
- const version = "1.0.20";
2695
- class Api {
3222
+ const version = "1.1.0-rc1";
3223
+ const _Api = class _Api {
2696
3224
  /**
2697
3225
  * Connect to Datalynk & send requests
2698
3226
  *
@@ -2701,10 +3229,10 @@ class Api {
2701
3229
  * const api = new Api('https://spoke.auxiliumgroup.com');
2702
3230
  * ```
2703
3231
  *
2704
- * @param {string} url API URL
3232
+ * @param {string} origin API URL
2705
3233
  * @param {ApiOptions} options
2706
3234
  */
2707
- constructor(url, options = {}) {
3235
+ constructor(origin, options = {}) {
2708
3236
  /** Current requests bundle */
2709
3237
  __publicField(this, "bundle", []);
2710
3238
  /** Bundle lifecycle tracking */
@@ -2713,12 +3241,6 @@ class Api {
2713
3241
  __publicField(this, "localStorageKey", "datalynk-token");
2714
3242
  /** Pending requests cache */
2715
3243
  __publicField(this, "pending", {});
2716
- /** API URL */
2717
- __publicField(this, "url");
2718
- /** Package version */
2719
- __publicField(this, "version", version);
2720
- /** API Session token */
2721
- __publicField(this, "token$", new BehaviorSubject(void 0));
2722
3244
  /** Helpers */
2723
3245
  /** Authentication */
2724
3246
  __publicField(this, "auth");
@@ -2730,12 +3252,26 @@ class Api {
2730
3252
  __publicField(this, "socket");
2731
3253
  /** Superuser */
2732
3254
  __publicField(this, "superuser");
2733
- this.options = options;
2734
- this.url = `${new URL(url).origin}/api/`;
3255
+ /** Offline database */
3256
+ __publicField(this, "database");
3257
+ /** Options */
3258
+ __publicField(this, "options");
3259
+ /** Created slices */
3260
+ __publicField(this, "sliceCache", /* @__PURE__ */ new Map());
3261
+ /** API URL */
3262
+ __publicField(this, "url");
3263
+ /** Client library version */
3264
+ __publicField(this, "version", version);
3265
+ /** API Session token */
3266
+ __publicField(this, "token$", new BehaviorSubject(void 0));
3267
+ var _a, _b;
3268
+ this.origin = origin;
3269
+ this.url = `${new URL(origin).origin}/api/`;
2735
3270
  this.options = {
2736
- bundleTime: 100,
3271
+ offline: [],
2737
3272
  origin: typeof location !== "undefined" ? location.host : "Unknown",
2738
3273
  saveSession: true,
3274
+ serviceWorker: "/service.worker.js",
2739
3275
  ...options
2740
3276
  };
2741
3277
  if (this.options.saveSession) {
@@ -2746,12 +3282,32 @@ class Api {
2746
3282
  else localStorage.removeItem(this.localStorageKey);
2747
3283
  });
2748
3284
  }
2749
- Slice.api = this;
3285
+ this.socket = new Socket(this, { url: options.socket });
2750
3286
  this.auth = new Auth(this);
2751
3287
  this.files = new Files(this);
2752
3288
  this.pdf = new Pdf(this);
2753
3289
  this.superuser = new Superuser(this);
2754
- this.socket = new Socket(this, { url: options.socket });
3290
+ if ((_a = this.options.offline) == null ? void 0 : _a.length) {
3291
+ if (typeof indexedDB == "undefined") throw new Error("Cannot enable offline support, indexedDB is not available in this environment");
3292
+ this.database = new Database("datalynk", this.options.offline);
3293
+ (_b = this.options.offline) == null ? void 0 : _b.forEach((id) => this.slice(id));
3294
+ this.cacheUrl();
3295
+ }
3296
+ }
3297
+ /** Get session info from JWT payload */
3298
+ get jwtPayload() {
3299
+ if (!this.token) return null;
3300
+ return decodeJwt(this.token);
3301
+ }
3302
+ /** Is token expired */
3303
+ get expired() {
3304
+ var _a;
3305
+ return (((_a = this.jwtPayload) == null ? void 0 : _a.exp) ?? Infinity) * 1e3 <= Date.now();
3306
+ }
3307
+ /** Logged in spoke */
3308
+ get spoke() {
3309
+ var _a;
3310
+ return (_a = this.jwtPayload) == null ? void 0 : _a.realm;
2755
3311
  }
2756
3312
  get token() {
2757
3313
  return this.token$.getValue();
@@ -2759,11 +3315,6 @@ class Api {
2759
3315
  set token(token) {
2760
3316
  this.token$.next(token);
2761
3317
  }
2762
- /** Get session info from JWT payload */
2763
- get jwtPayload() {
2764
- if (!this.token) return null;
2765
- return decodeJwt(this.token);
2766
- }
2767
3318
  _request(req, options = {}) {
2768
3319
  const token = options.token || this.token;
2769
3320
  return fetch(this.url, {
@@ -2773,14 +3324,31 @@ class Api {
2773
3324
  "Content-Type": "application/json",
2774
3325
  "X-Date-Return-Format": this.options.legacyDates ? void 0 : "ISO8601"
2775
3326
  }),
2776
- body: JSON.stringify(Api.translateTokens(req))
3327
+ body: JSON.stringify(_Api.translateTokens(req))
2777
3328
  }).then(async (resp) => {
2778
3329
  let data = JSONAttemptParse(await resp.text());
2779
3330
  if (!resp.ok || (data == null ? void 0 : data.error)) throw Object.assign(errorFromCode(resp.status, data.error), data);
2780
- if (!options.raw) data = Api.translateTokens(data);
3331
+ if (!options.raw) data = _Api.translateTokens(data);
2781
3332
  return data;
2782
3333
  });
2783
3334
  }
3335
+ async cacheUrl() {
3336
+ if (!this.options.serviceWorker || !("serviceWorker" in navigator)) return;
3337
+ await navigator.serviceWorker.getRegistration(this.options.serviceWorker).then((reg) => reg ?? navigator.serviceWorker.register(this.options.serviceWorker, { scope: "/" }));
3338
+ await navigator.serviceWorker.ready;
3339
+ if (!navigator.serviceWorker.controller) return window.location.reload();
3340
+ }
3341
+ /**
3342
+ * Get list of slices
3343
+ * @return {Promise<number[]>}
3344
+ */
3345
+ getSlices() {
3346
+ return this.request({ "$/tools/action_chain": [
3347
+ { "!/env/me": {} },
3348
+ { "!/slice/permissionsLite": {} },
3349
+ { "!/tools/column": { "col": "id", "rows": { "$_": "1:rows" } } }
3350
+ ] });
3351
+ }
2784
3352
  /**
2785
3353
  * Parses API request/response object for special Datalynk tokens & converts them to native JS objects
2786
3354
  *
@@ -2799,6 +3367,10 @@ class Api {
2799
3367
  val = Serializer.deserialize[index](val[index]);
2800
3368
  } else if (index in Serializer.serialize) {
2801
3369
  val = Serializer.serialize[index](val[index]);
3370
+ } else if (val[index] == "Yes") {
3371
+ val[index] = true;
3372
+ } else if (val[index] == "No") {
3373
+ val[index] = false;
2802
3374
  } else {
2803
3375
  queue.push(`${key}.${index}`);
2804
3376
  }
@@ -2815,7 +3387,7 @@ class Api {
2815
3387
  chain(...requests) {
2816
3388
  const arr = requests.length == 1 && Array.isArray(requests[0]) ? requests[0] : requests;
2817
3389
  return this.request({ "$/tools/action_chain": arr.map((r) => {
2818
- if (!(r instanceof Slice)) return r;
3390
+ if (!(r instanceof ApiCall)) return r;
2819
3391
  const req = r.raw;
2820
3392
  Object.keys(req).forEach((key) => {
2821
3393
  if (key.startsWith("$/")) {
@@ -2834,7 +3406,7 @@ class Api {
2834
3406
  chainMap(request) {
2835
3407
  return this.request({ "$/tools/do": [
2836
3408
  ...Object.entries(request).flatMap(([key, r]) => {
2837
- if (!(r instanceof Slice)) return [key, r];
3409
+ if (!(r instanceof ApiCall)) return [key, r];
2838
3410
  const req = r.raw;
2839
3411
  Object.keys(req).forEach((key2) => {
2840
3412
  if (key2.startsWith("$/")) {
@@ -2915,15 +3487,20 @@ class Api {
2915
3487
  * const unsubscribe = contactsSlice.sync().subscribe(rows => console.log(rows));
2916
3488
  * ```
2917
3489
  *
2918
- * @param {number} slice Slice number the object will target
3490
+ * @param {number} id Slice ID the object will target
2919
3491
  * @returns {Slice<T = any>} Object for making requests & caching rows
2920
3492
  */
2921
- slice(slice) {
2922
- return new Slice(slice, this);
3493
+ slice(id) {
3494
+ if (!this.sliceCache.has(+id)) this.sliceCache.set(+id, new Slice(id, this));
3495
+ return this.sliceCache.get(+id);
2923
3496
  }
2924
- }
3497
+ };
3498
+ /** Client library version */
3499
+ __publicField(_Api, "version", version);
3500
+ let Api = _Api;
2925
3501
  export {
2926
3502
  Api,
3503
+ ApiCall,
2927
3504
  Auth,
2928
3505
  Files,
2929
3506
  LoginPrompt,
@@ -2931,5 +3508,6 @@ export {
2931
3508
  Serializer,
2932
3509
  Slice,
2933
3510
  Socket,
2934
- Superuser
3511
+ Superuser,
3512
+ getTheme
2935
3513
  };