@webority/mobile-core 0.0.23

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.
Files changed (211) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +39 -0
  3. package/android/build.gradle +65 -0
  4. package/android/src/newarch/java/com/webority/mobilecore/smsretriever/AppSignatureHelper.kt +57 -0
  5. package/android/src/newarch/java/com/webority/mobilecore/smsretriever/SmsRetrieverModule.kt +20 -0
  6. package/android/src/newarch/java/com/webority/mobilecore/smsretriever/SmsRetrieverModuleImpl.kt +146 -0
  7. package/android/src/newarch/java/com/webority/mobilecore/smsretriever/SmsRetrieverPackage.kt +33 -0
  8. package/lib/commonjs/api/baseUrl.js +34 -0
  9. package/lib/commonjs/api/curl.js +109 -0
  10. package/lib/commonjs/api/publicClient.js +73 -0
  11. package/lib/commonjs/api/retry.js +75 -0
  12. package/lib/commonjs/api/tokenRefresh.js +205 -0
  13. package/lib/commonjs/api/userClient.js +255 -0
  14. package/lib/commonjs/auth/authStore.js +232 -0
  15. package/lib/commonjs/auth/index.js +44 -0
  16. package/lib/commonjs/auth/jwt.js +112 -0
  17. package/lib/commonjs/config/index.js +66 -0
  18. package/lib/commonjs/deepLink/index.js +286 -0
  19. package/lib/commonjs/download/adapters/expoFs.js +69 -0
  20. package/lib/commonjs/download/adapters/shared.js +13 -0
  21. package/lib/commonjs/download/index.js +250 -0
  22. package/lib/commonjs/formatters/currency.js +69 -0
  23. package/lib/commonjs/formatters/date.js +168 -0
  24. package/lib/commonjs/formatters/dateMath.js +310 -0
  25. package/lib/commonjs/formatters/index.js +246 -0
  26. package/lib/commonjs/formatters/initials.js +32 -0
  27. package/lib/commonjs/formatters/number.js +88 -0
  28. package/lib/commonjs/formatters/phone.js +369 -0
  29. package/lib/commonjs/formatters/windowsToIanaTable.js +164 -0
  30. package/lib/commonjs/geolocation/adapters/expoLocation.js +126 -0
  31. package/lib/commonjs/geolocation/index.js +355 -0
  32. package/lib/commonjs/index.js +630 -0
  33. package/lib/commonjs/initSDK.js +66 -0
  34. package/lib/commonjs/initUserAuth.js +37 -0
  35. package/lib/commonjs/logger/index.js +291 -0
  36. package/lib/commonjs/network/index.js +31 -0
  37. package/lib/commonjs/network/networkStatus.js +133 -0
  38. package/lib/commonjs/package.json +1 -0
  39. package/lib/commonjs/permissions/calendar.js +14 -0
  40. package/lib/commonjs/permissions/camera.js +14 -0
  41. package/lib/commonjs/permissions/contacts.js +10 -0
  42. package/lib/commonjs/permissions/index.js +133 -0
  43. package/lib/commonjs/permissions/location.js +14 -0
  44. package/lib/commonjs/permissions/mediaLibrary.js +10 -0
  45. package/lib/commonjs/permissions/notifications.js +10 -0
  46. package/lib/commonjs/permissions/photos.js +14 -0
  47. package/lib/commonjs/phone/countries.js +265 -0
  48. package/lib/commonjs/share/index.js +70 -0
  49. package/lib/commonjs/smsRetriever/index.js +91 -0
  50. package/lib/commonjs/specs/NativeSmsRetriever.js +15 -0
  51. package/lib/commonjs/sqlite/adapters/expo.js +53 -0
  52. package/lib/commonjs/sqlite/index.js +604 -0
  53. package/lib/commonjs/storage/index.js +214 -0
  54. package/lib/commonjs/types/globals.d.js +6 -0
  55. package/lib/commonjs/utils/imageCompression.js +389 -0
  56. package/lib/commonjs/utils/index.js +19 -0
  57. package/lib/commonjs/validators/index.js +227 -0
  58. package/lib/commonjs/versionCheck/index.js +419 -0
  59. package/lib/module/api/baseUrl.js +30 -0
  60. package/lib/module/api/curl.js +104 -0
  61. package/lib/module/api/publicClient.js +69 -0
  62. package/lib/module/api/retry.js +69 -0
  63. package/lib/module/api/tokenRefresh.js +192 -0
  64. package/lib/module/api/userClient.js +233 -0
  65. package/lib/module/auth/authStore.js +224 -0
  66. package/lib/module/auth/index.js +5 -0
  67. package/lib/module/auth/jwt.js +106 -0
  68. package/lib/module/config/index.js +58 -0
  69. package/lib/module/deepLink/index.js +280 -0
  70. package/lib/module/download/adapters/expoFs.js +64 -0
  71. package/lib/module/download/adapters/shared.js +8 -0
  72. package/lib/module/download/index.js +242 -0
  73. package/lib/module/formatters/currency.js +64 -0
  74. package/lib/module/formatters/date.js +160 -0
  75. package/lib/module/formatters/dateMath.js +294 -0
  76. package/lib/module/formatters/index.js +9 -0
  77. package/lib/module/formatters/initials.js +27 -0
  78. package/lib/module/formatters/number.js +81 -0
  79. package/lib/module/formatters/phone.js +302 -0
  80. package/lib/module/formatters/windowsToIanaTable.js +160 -0
  81. package/lib/module/geolocation/adapters/expoLocation.js +121 -0
  82. package/lib/module/geolocation/index.js +347 -0
  83. package/lib/module/index.js +54 -0
  84. package/lib/module/initSDK.js +61 -0
  85. package/lib/module/initUserAuth.js +33 -0
  86. package/lib/module/logger/index.js +285 -0
  87. package/lib/module/network/index.js +4 -0
  88. package/lib/module/network/networkStatus.js +124 -0
  89. package/lib/module/package.json +1 -0
  90. package/lib/module/permissions/calendar.js +15 -0
  91. package/lib/module/permissions/camera.js +15 -0
  92. package/lib/module/permissions/contacts.js +11 -0
  93. package/lib/module/permissions/index.js +128 -0
  94. package/lib/module/permissions/location.js +15 -0
  95. package/lib/module/permissions/mediaLibrary.js +11 -0
  96. package/lib/module/permissions/notifications.js +11 -0
  97. package/lib/module/permissions/photos.js +15 -0
  98. package/lib/module/phone/countries.js +261 -0
  99. package/lib/module/share/index.js +64 -0
  100. package/lib/module/smsRetriever/index.js +86 -0
  101. package/lib/module/specs/NativeSmsRetriever.js +13 -0
  102. package/lib/module/sqlite/adapters/expo.js +48 -0
  103. package/lib/module/sqlite/index.js +594 -0
  104. package/lib/module/storage/index.js +208 -0
  105. package/lib/module/types/globals.d.js +10 -0
  106. package/lib/module/utils/imageCompression.js +383 -0
  107. package/lib/module/utils/index.js +4 -0
  108. package/lib/module/validators/index.js +205 -0
  109. package/lib/module/versionCheck/index.js +413 -0
  110. package/lib/typescript/commonjs/api/baseUrl.d.ts +20 -0
  111. package/lib/typescript/commonjs/api/curl.d.ts +18 -0
  112. package/lib/typescript/commonjs/api/publicClient.d.ts +11 -0
  113. package/lib/typescript/commonjs/api/retry.d.ts +27 -0
  114. package/lib/typescript/commonjs/api/tokenRefresh.d.ts +43 -0
  115. package/lib/typescript/commonjs/api/userClient.d.ts +37 -0
  116. package/lib/typescript/commonjs/auth/authStore.d.ts +79 -0
  117. package/lib/typescript/commonjs/auth/index.d.ts +5 -0
  118. package/lib/typescript/commonjs/auth/jwt.d.ts +31 -0
  119. package/lib/typescript/commonjs/config/index.d.ts +55 -0
  120. package/lib/typescript/commonjs/deepLink/index.d.ts +54 -0
  121. package/lib/typescript/commonjs/download/adapters/expoFs.d.ts +38 -0
  122. package/lib/typescript/commonjs/download/adapters/shared.d.ts +3 -0
  123. package/lib/typescript/commonjs/download/index.d.ts +86 -0
  124. package/lib/typescript/commonjs/formatters/currency.d.ts +24 -0
  125. package/lib/typescript/commonjs/formatters/date.d.ts +22 -0
  126. package/lib/typescript/commonjs/formatters/dateMath.d.ts +88 -0
  127. package/lib/typescript/commonjs/formatters/index.d.ts +10 -0
  128. package/lib/typescript/commonjs/formatters/initials.d.ts +11 -0
  129. package/lib/typescript/commonjs/formatters/number.d.ts +15 -0
  130. package/lib/typescript/commonjs/formatters/phone.d.ts +133 -0
  131. package/lib/typescript/commonjs/formatters/windowsToIanaTable.d.ts +18 -0
  132. package/lib/typescript/commonjs/geolocation/adapters/expoLocation.d.ts +42 -0
  133. package/lib/typescript/commonjs/geolocation/index.d.ts +90 -0
  134. package/lib/typescript/commonjs/index.d.ts +54 -0
  135. package/lib/typescript/commonjs/initSDK.d.ts +72 -0
  136. package/lib/typescript/commonjs/initUserAuth.d.ts +41 -0
  137. package/lib/typescript/commonjs/logger/index.d.ts +81 -0
  138. package/lib/typescript/commonjs/network/index.d.ts +3 -0
  139. package/lib/typescript/commonjs/network/networkStatus.d.ts +43 -0
  140. package/lib/typescript/commonjs/package.json +1 -0
  141. package/lib/typescript/commonjs/permissions/calendar.d.ts +2 -0
  142. package/lib/typescript/commonjs/permissions/camera.d.ts +2 -0
  143. package/lib/typescript/commonjs/permissions/contacts.d.ts +2 -0
  144. package/lib/typescript/commonjs/permissions/index.d.ts +48 -0
  145. package/lib/typescript/commonjs/permissions/location.d.ts +2 -0
  146. package/lib/typescript/commonjs/permissions/mediaLibrary.d.ts +2 -0
  147. package/lib/typescript/commonjs/permissions/notifications.d.ts +2 -0
  148. package/lib/typescript/commonjs/permissions/photos.d.ts +2 -0
  149. package/lib/typescript/commonjs/phone/countries.d.ts +13 -0
  150. package/lib/typescript/commonjs/share/index.d.ts +29 -0
  151. package/lib/typescript/commonjs/smsRetriever/index.d.ts +30 -0
  152. package/lib/typescript/commonjs/specs/NativeSmsRetriever.d.ts +15 -0
  153. package/lib/typescript/commonjs/sqlite/adapters/expo.d.ts +30 -0
  154. package/lib/typescript/commonjs/sqlite/index.d.ts +113 -0
  155. package/lib/typescript/commonjs/storage/index.d.ts +64 -0
  156. package/lib/typescript/commonjs/utils/imageCompression.d.ts +152 -0
  157. package/lib/typescript/commonjs/utils/index.d.ts +3 -0
  158. package/lib/typescript/commonjs/validators/index.d.ts +51 -0
  159. package/lib/typescript/commonjs/versionCheck/index.d.ts +80 -0
  160. package/lib/typescript/module/api/baseUrl.d.ts +20 -0
  161. package/lib/typescript/module/api/curl.d.ts +18 -0
  162. package/lib/typescript/module/api/publicClient.d.ts +11 -0
  163. package/lib/typescript/module/api/retry.d.ts +27 -0
  164. package/lib/typescript/module/api/tokenRefresh.d.ts +43 -0
  165. package/lib/typescript/module/api/userClient.d.ts +37 -0
  166. package/lib/typescript/module/auth/authStore.d.ts +79 -0
  167. package/lib/typescript/module/auth/index.d.ts +5 -0
  168. package/lib/typescript/module/auth/jwt.d.ts +31 -0
  169. package/lib/typescript/module/config/index.d.ts +55 -0
  170. package/lib/typescript/module/deepLink/index.d.ts +54 -0
  171. package/lib/typescript/module/download/adapters/expoFs.d.ts +38 -0
  172. package/lib/typescript/module/download/adapters/shared.d.ts +3 -0
  173. package/lib/typescript/module/download/index.d.ts +86 -0
  174. package/lib/typescript/module/formatters/currency.d.ts +24 -0
  175. package/lib/typescript/module/formatters/date.d.ts +22 -0
  176. package/lib/typescript/module/formatters/dateMath.d.ts +88 -0
  177. package/lib/typescript/module/formatters/index.d.ts +10 -0
  178. package/lib/typescript/module/formatters/initials.d.ts +11 -0
  179. package/lib/typescript/module/formatters/number.d.ts +15 -0
  180. package/lib/typescript/module/formatters/phone.d.ts +133 -0
  181. package/lib/typescript/module/formatters/windowsToIanaTable.d.ts +18 -0
  182. package/lib/typescript/module/geolocation/adapters/expoLocation.d.ts +42 -0
  183. package/lib/typescript/module/geolocation/index.d.ts +90 -0
  184. package/lib/typescript/module/index.d.ts +54 -0
  185. package/lib/typescript/module/initSDK.d.ts +72 -0
  186. package/lib/typescript/module/initUserAuth.d.ts +41 -0
  187. package/lib/typescript/module/logger/index.d.ts +81 -0
  188. package/lib/typescript/module/network/index.d.ts +3 -0
  189. package/lib/typescript/module/network/networkStatus.d.ts +43 -0
  190. package/lib/typescript/module/package.json +1 -0
  191. package/lib/typescript/module/permissions/calendar.d.ts +2 -0
  192. package/lib/typescript/module/permissions/camera.d.ts +2 -0
  193. package/lib/typescript/module/permissions/contacts.d.ts +2 -0
  194. package/lib/typescript/module/permissions/index.d.ts +48 -0
  195. package/lib/typescript/module/permissions/location.d.ts +2 -0
  196. package/lib/typescript/module/permissions/mediaLibrary.d.ts +2 -0
  197. package/lib/typescript/module/permissions/notifications.d.ts +2 -0
  198. package/lib/typescript/module/permissions/photos.d.ts +2 -0
  199. package/lib/typescript/module/phone/countries.d.ts +13 -0
  200. package/lib/typescript/module/share/index.d.ts +29 -0
  201. package/lib/typescript/module/smsRetriever/index.d.ts +30 -0
  202. package/lib/typescript/module/specs/NativeSmsRetriever.d.ts +15 -0
  203. package/lib/typescript/module/sqlite/adapters/expo.d.ts +30 -0
  204. package/lib/typescript/module/sqlite/index.d.ts +113 -0
  205. package/lib/typescript/module/storage/index.d.ts +64 -0
  206. package/lib/typescript/module/utils/imageCompression.d.ts +152 -0
  207. package/lib/typescript/module/utils/index.d.ts +3 -0
  208. package/lib/typescript/module/validators/index.d.ts +51 -0
  209. package/lib/typescript/module/versionCheck/index.d.ts +80 -0
  210. package/package.json +298 -0
  211. package/src/specs/NativeSmsRetriever.ts +16 -0
@@ -0,0 +1,594 @@
1
+ "use strict";
2
+
3
+ import * as ExpoSqlite from 'expo-sqlite';
4
+ import { Logger } from "../logger/index.js";
5
+ import { adaptExpoSqlite } from "./adapters/expo.js";
6
+
7
+ /**
8
+ * expo-sqlite is the fleet's only sanctioned SQLite driver (react-native.md:
9
+ * "we will always use expo-sqlite, what Expo gives, we don't look elsewhere
10
+ * ever") — this module resolves it automatically, with no app-side wiring.
11
+ * `setSqliteImplementation` still exists, narrowed to tests and the showcase:
12
+ * it injects a denying/failing backend to reach error paths a healthy device
13
+ * never takes, the same pattern every other injectable surface in this
14
+ * library follows. It is not a way to pick an alternate real driver.
15
+ */
16
+
17
+ /** Only what a test or an app injected; null means use expo-sqlite. */
18
+ let backend = null;
19
+
20
+ /** expo-sqlite, adapted once and memoised. The only driver this module resolves. */
21
+ let expoDriver;
22
+
23
+ /**
24
+ * Test/showcase-only injection point — see the module header. Not a way to
25
+ * pick an alternate real driver; `@webority/mobile-core` always
26
+ * resolves expo-sqlite when nothing is injected. Pass `null` to clear it.
27
+ */
28
+ export const setSqliteImplementation = impl => {
29
+ backend = impl;
30
+ };
31
+
32
+ /**
33
+ * expo-sqlite is a required peer, so the JS module always resolves. What can be
34
+ * missing is the NATIVE side: an app that installed the package without running
35
+ * the native install gets a module without the one call the adapter needs.
36
+ * Checking it is what lets `isSqliteAvailable` mean something and makes the
37
+ * error below reachable.
38
+ *
39
+ * An INJECTED driver is taken on trust, like every other seam in this package.
40
+ */
41
+ const hasNativeSqlite = mod => typeof mod?.openDatabaseAsync === 'function';
42
+ const resolveDriver = () => {
43
+ if (backend) {
44
+ return backend;
45
+ }
46
+ if (expoDriver === undefined) {
47
+ const mod = ExpoSqlite;
48
+ expoDriver = hasNativeSqlite(mod) ? adaptExpoSqlite(mod) : null;
49
+ }
50
+ return expoDriver;
51
+ };
52
+ export const isSqliteAvailable = () => resolveDriver() !== null;
53
+ const requireBackend = op => {
54
+ const b = resolveDriver();
55
+ if (!b) {
56
+ const err = new Error(`[@webority/mobile-core] No SQLite driver available; cannot ${op}. ` + 'Install expo-sqlite (the only sanctioned driver) in the consuming app.');
57
+ Logger.error(err.message);
58
+ throw err;
59
+ }
60
+ return b;
61
+ };
62
+
63
+ /** The ledger of applied migrations. Never emptied by `wipe`. */
64
+ export const MIGRATIONS_TABLE = '_webority_migrations';
65
+ const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
66
+
67
+ /**
68
+ * SQLite cannot bind an identifier to `?`, so every table/column name reaching
69
+ * a statement is concatenated. This is the ONE place that is allowed to happen,
70
+ * and it throws rather than quoting a hostile name into something executable.
71
+ */
72
+ export const assertSqlIdentifier = (value, kind = 'identifier') => {
73
+ if (typeof value !== 'string' || !IDENTIFIER.test(value)) {
74
+ throw new Error(`[@webority/mobile-core] Invalid SQL ${kind} ${JSON.stringify(value)}. ` + 'Only letters, digits and underscores are allowed, and it must not start with a digit. ' + 'Pass values as bound parameters instead of building them into the name.');
75
+ }
76
+ return value;
77
+ };
78
+ const quoteIdentifier = (value, kind = 'identifier') => `"${assertSqlIdentifier(value, kind)}"`;
79
+
80
+ /** For names read back out of sqlite_master, which may legitimately be quoted. */
81
+ const escapeIdentifier = value => `"${value.replace(/"/g, '""')}"`;
82
+ const COLUMN_DEFINITION = /^[A-Za-z0-9_ ,.()'"+*/=<>|%:$-]+$/;
83
+ const assertColumnDefinition = (definition, column) => {
84
+ const trimmed = typeof definition === 'string' ? definition.trim() : '';
85
+ if (trimmed.length === 0 || !COLUMN_DEFINITION.test(trimmed) || trimmed.includes('--')) {
86
+ throw new Error(`[@webority/mobile-core] Invalid column definition for "${column}": ` + `${JSON.stringify(definition)}. A definition is raw SQL such as ` + "'INTEGER NOT NULL DEFAULT 0' and may not contain a statement separator or a comment.");
87
+ }
88
+ return trimmed;
89
+ };
90
+
91
+ /** Normalise a JS value into something every SQLite driver can bind. */
92
+ export const toSqliteValue = value => {
93
+ if (value === undefined) {
94
+ return null;
95
+ }
96
+ if (typeof value === 'boolean') {
97
+ return value ? 1 : 0;
98
+ }
99
+ if (value instanceof Date) {
100
+ return value.toISOString();
101
+ }
102
+ return value;
103
+ };
104
+
105
+ /** SQLite has no boolean type, so a stored flag comes back as 0/1 or '0'/'1'. */
106
+ export const fromSqliteBoolean = value => {
107
+ if (typeof value === 'boolean') {
108
+ return value;
109
+ }
110
+ if (typeof value === 'number') {
111
+ return value !== 0;
112
+ }
113
+ if (typeof value === 'string') {
114
+ const normalised = value.trim().toLowerCase();
115
+ return normalised === '1' || normalised === 'true';
116
+ }
117
+ return false;
118
+ };
119
+
120
+ /**
121
+ * Build an `IN (...)` clause with one placeholder per value. An empty list
122
+ * yields a clause that matches nothing, because `IN ()` is a syntax error.
123
+ */
124
+ export const whereIn = (column, values) => {
125
+ const quoted = quoteIdentifier(column, 'column name');
126
+ if (values.length === 0) {
127
+ return {
128
+ sql: '1 = 0',
129
+ params: []
130
+ };
131
+ }
132
+ return {
133
+ sql: `${quoted} IN (${values.map(() => '?').join(', ')})`,
134
+ params: values.map(toSqliteValue)
135
+ };
136
+ };
137
+
138
+ /**
139
+ * Either a map of column to value (ANDed, `null` meaning IS NULL) or a raw
140
+ * fragment for anything the object form cannot express.
141
+ */
142
+
143
+ const isRawWhere = where => typeof where.sql === 'string' && Object.keys(where).every(key => key === 'sql' || key === 'params');
144
+ const buildWhere = where => {
145
+ if (!where) {
146
+ return {
147
+ clause: '',
148
+ params: []
149
+ };
150
+ }
151
+ if (isRawWhere(where)) {
152
+ const sql = where.sql.trim();
153
+ if (sql.length === 0) {
154
+ return {
155
+ clause: '',
156
+ params: []
157
+ };
158
+ }
159
+ return {
160
+ clause: ` WHERE ${sql}`,
161
+ params: [...(where.params ?? [])]
162
+ };
163
+ }
164
+ const entries = Object.entries(where);
165
+ if (entries.length === 0) {
166
+ return {
167
+ clause: '',
168
+ params: []
169
+ };
170
+ }
171
+ const parts = [];
172
+ const params = [];
173
+ for (const [column, rawValue] of entries) {
174
+ const quoted = quoteIdentifier(column, 'column name');
175
+ const value = toSqliteValue(rawValue);
176
+ if (value === null) {
177
+ parts.push(`${quoted} IS NULL`);
178
+ } else {
179
+ parts.push(`${quoted} = ?`);
180
+ params.push(value);
181
+ }
182
+ }
183
+ return {
184
+ clause: ` WHERE ${parts.join(' AND ')}`,
185
+ params
186
+ };
187
+ };
188
+ const buildOrderBy = orderBy => {
189
+ if (orderBy === undefined) {
190
+ return '';
191
+ }
192
+ const terms = orderBy.split(',').map(term => term.trim()).filter(term => term.length > 0);
193
+ if (terms.length === 0) {
194
+ return '';
195
+ }
196
+ const rendered = terms.map(term => {
197
+ const [column, direction, ...rest] = term.split(/\s+/);
198
+ if (rest.length > 0) {
199
+ throw new Error(`[@webority/mobile-core] Invalid orderBy term ${JSON.stringify(term)}. ` + 'Expected a column name optionally followed by ASC or DESC.');
200
+ }
201
+ const quoted = quoteIdentifier(column, 'orderBy column');
202
+ if (direction === undefined) {
203
+ return quoted;
204
+ }
205
+ const upper = direction.toUpperCase();
206
+ if (upper !== 'ASC' && upper !== 'DESC') {
207
+ throw new Error(`[@webority/mobile-core] Invalid sort direction ${JSON.stringify(direction)} ` + `in orderBy ${JSON.stringify(orderBy)}. Expected ASC or DESC.`);
208
+ }
209
+ return `${quoted} ${upper}`;
210
+ });
211
+ return ` ORDER BY ${rendered.join(', ')}`;
212
+ };
213
+ const assertNonNegativeInteger = (value, kind) => {
214
+ if (!Number.isInteger(value) || value < 0) {
215
+ throw new Error(`[@webority/mobile-core] Invalid ${kind} ${JSON.stringify(value)}. ` + 'Expected a non-negative integer.');
216
+ }
217
+ return value;
218
+ };
219
+ /**
220
+ * FNV-1a with the length mixed in, so two different bodies must also share a
221
+ * length before they can be mistaken for one another. Deliberately hand-rolled:
222
+ * this module ships with zero dependencies.
223
+ */
224
+ const hashString = value => {
225
+ let hash = 0x811c9dc5;
226
+ for (let index = 0; index < value.length; index += 1) {
227
+ hash ^= value.charCodeAt(index);
228
+ hash = Math.imul(hash, 0x01000193) >>> 0;
229
+ }
230
+ return `${hash.toString(16).padStart(8, '0')}-${value.length.toString(16)}`;
231
+ };
232
+
233
+ /**
234
+ * A function migration has no stable text: release bundles are minified, so
235
+ * hashing its source would report a mismatch on every production build. Its
236
+ * body is therefore unverifiable, and only the switch between a function and
237
+ * SQL is detected.
238
+ */
239
+ const migrationChecksum = migration => {
240
+ if (typeof migration.up === 'function') {
241
+ return hashString('fn');
242
+ }
243
+ const sql = Array.isArray(migration.up) ? migration.up.join('\n') : migration.up;
244
+ return hashString(sql);
245
+ };
246
+ export const DEFAULT_PRAGMAS = {
247
+ journal_mode: 'WAL',
248
+ synchronous: 'NORMAL',
249
+ foreign_keys: 'ON',
250
+ busy_timeout: 5000
251
+ };
252
+ const PRAGMA_VALUE = /^[A-Za-z0-9_-]+$/;
253
+ const renderPragmaValue = (name, value) => {
254
+ if (typeof value === 'number') {
255
+ if (!Number.isFinite(value)) {
256
+ throw new Error(`[@webority/mobile-core] Invalid value for PRAGMA ${name}: ${value}.`);
257
+ }
258
+ return String(value);
259
+ }
260
+ if (!PRAGMA_VALUE.test(value)) {
261
+ throw new Error(`[@webority/mobile-core] Invalid value ${JSON.stringify(value)} for ` + `PRAGMA ${name}. PRAGMA values cannot be bound, so only letters, digits, ` + 'underscores and hyphens are accepted.');
262
+ }
263
+ return value;
264
+ };
265
+ const createDb = (name, raw) => {
266
+ // SQLite has ONE transaction per connection, so two concurrent transaction()
267
+ // calls would interleave their BEGIN/COMMIT and the second BEGIN would be
268
+ // rejected outright. An outbox flush racing a UI write is exactly that case,
269
+ // so transactions queue on this chain rather than run concurrently.
270
+ let txQueue = Promise.resolve();
271
+ const execute = async (sql, params) => {
272
+ try {
273
+ return await raw.execute(sql, params);
274
+ } catch (error) {
275
+ Logger.error(`[@webority/mobile-core] SQLite execute failed: ${sql}`, error);
276
+ throw error;
277
+ }
278
+ };
279
+ const select = async (sql, params) => {
280
+ const result = await execute(sql, params);
281
+ return result.rows;
282
+ };
283
+ const selectOne = async (sql, params) => {
284
+ const rows = await select(sql, params);
285
+ return rows.length > 0 ? rows[0] : null;
286
+ };
287
+ const runTransaction = async work => {
288
+ await raw.execute('BEGIN');
289
+ let result;
290
+ try {
291
+ result = await work({
292
+ execute,
293
+ select,
294
+ selectOne
295
+ });
296
+ } catch (error) {
297
+ try {
298
+ await raw.execute('ROLLBACK');
299
+ } catch (rollbackError) {
300
+ Logger.error('[@webority/mobile-core] SQLite ROLLBACK failed', rollbackError);
301
+ }
302
+ throw error;
303
+ }
304
+ try {
305
+ await raw.execute('COMMIT');
306
+ } catch (commitError) {
307
+ // A failed COMMIT leaves the transaction open, which would poison every
308
+ // later one on this connection. Roll back before surfacing it.
309
+ try {
310
+ await raw.execute('ROLLBACK');
311
+ } catch {
312
+ /* the connection is already in a bad state; the commit error is the useful one */
313
+ }
314
+ throw commitError;
315
+ }
316
+ return result;
317
+ };
318
+ const transaction = work => {
319
+ // A rejected link must not break the chain for everyone behind it.
320
+ const next = txQueue.then(() => runTransaction(work), () => runTransaction(work));
321
+ txQueue = next.catch(() => undefined);
322
+ return next;
323
+ };
324
+ const insertSql = (table, row) => {
325
+ const quotedTable = quoteIdentifier(table, 'table name');
326
+ const entries = Object.entries(row);
327
+ if (entries.length === 0) {
328
+ throw new Error(`[@webority/mobile-core] Cannot insert an empty row into ${table}. ` + 'Pass at least one column.');
329
+ }
330
+ const columns = entries.map(([column]) => quoteIdentifier(column, 'column name'));
331
+ const placeholders = entries.map(() => '?');
332
+ return {
333
+ sql: `INSERT INTO ${quotedTable} (${columns.join(', ')}) VALUES (${placeholders.join(', ')})`,
334
+ params: entries.map(([, value]) => toSqliteValue(value))
335
+ };
336
+ };
337
+ const insert = async (table, row) => {
338
+ const {
339
+ sql,
340
+ params
341
+ } = insertSql(table, row);
342
+ const result = await execute(sql, params);
343
+ return result.insertId;
344
+ };
345
+ const insertMany = async (table, rows) => {
346
+ if (rows.length === 0) {
347
+ return 0;
348
+ }
349
+ // Built before the transaction opens so a bad identifier throws without
350
+ // leaving a BEGIN behind.
351
+ const statements = rows.map(row => insertSql(table, row));
352
+ await transaction(async tx => {
353
+ for (const statement of statements) {
354
+ await tx.execute(statement.sql, statement.params);
355
+ }
356
+ });
357
+ return rows.length;
358
+ };
359
+ const update = async (table, values, where) => {
360
+ const quotedTable = quoteIdentifier(table, 'table name');
361
+ const entries = Object.entries(values);
362
+ if (entries.length === 0) {
363
+ throw new Error(`[@webority/mobile-core] Cannot update ${table} with no values. ` + 'Pass at least one column to set.');
364
+ }
365
+ const assignments = entries.map(([column]) => `${quoteIdentifier(column, 'column name')} = ?`);
366
+ const params = entries.map(([, value]) => toSqliteValue(value));
367
+ const built = buildWhere(where);
368
+ const result = await execute(`UPDATE ${quotedTable} SET ${assignments.join(', ')}${built.clause}`, [...params, ...built.params]);
369
+ return result.rowsAffected;
370
+ };
371
+ const remove = async (table, where) => {
372
+ const quotedTable = quoteIdentifier(table, 'table name');
373
+ const built = buildWhere(where);
374
+ const result = await execute(`DELETE FROM ${quotedTable}${built.clause}`, built.params);
375
+ return result.rowsAffected;
376
+ };
377
+ const upsert = async (table, row, conflictColumns) => {
378
+ if (conflictColumns.length === 0) {
379
+ throw new Error(`[@webority/mobile-core] upsert on ${table} needs at least one conflict ` + 'column; without one SQLite has no key to match an existing row on.');
380
+ }
381
+ const {
382
+ sql,
383
+ params
384
+ } = insertSql(table, row);
385
+ const conflictSet = new Set(conflictColumns);
386
+ const targets = conflictColumns.map(column => quoteIdentifier(column, 'column name'));
387
+ const updates = Object.keys(row).filter(column => !conflictSet.has(column)).map(column => {
388
+ const quoted = quoteIdentifier(column, 'column name');
389
+ return `${quoted} = excluded.${quoted}`;
390
+ });
391
+ const action = updates.length > 0 ? `DO UPDATE SET ${updates.join(', ')}` : 'DO NOTHING';
392
+ await execute(`${sql} ON CONFLICT (${targets.join(', ')}) ${action}`, params);
393
+ };
394
+ const findAll = async (table, where, options) => {
395
+ const quotedTable = quoteIdentifier(table, 'table name');
396
+ const built = buildWhere(where);
397
+ const params = [...built.params];
398
+ let sql = `SELECT * FROM ${quotedTable}${built.clause}${buildOrderBy(options?.orderBy)}`;
399
+ if (options?.limit !== undefined) {
400
+ assertNonNegativeInteger(options.limit, 'limit');
401
+ sql += ' LIMIT ?';
402
+ params.push(options.limit);
403
+ } else if (options?.offset !== undefined) {
404
+ // SQLite rejects OFFSET without LIMIT; -1 is its documented "no limit".
405
+ sql += ' LIMIT -1';
406
+ }
407
+ if (options?.offset !== undefined) {
408
+ assertNonNegativeInteger(options.offset, 'offset');
409
+ sql += ' OFFSET ?';
410
+ params.push(options.offset);
411
+ }
412
+ return select(sql, params);
413
+ };
414
+ const findOne = async (table, where) => {
415
+ const rows = await findAll(table, where, {
416
+ limit: 1
417
+ });
418
+ return rows.length > 0 ? rows[0] : null;
419
+ };
420
+ const count = async (table, where) => {
421
+ const quotedTable = quoteIdentifier(table, 'table name');
422
+ const built = buildWhere(where);
423
+ const row = await selectOne(`SELECT COUNT(*) AS cnt FROM ${quotedTable}${built.clause}`, built.params);
424
+ return Number(row?.cnt ?? 0);
425
+ };
426
+ const tableExists = async table => {
427
+ assertSqlIdentifier(table, 'table name');
428
+ const row = await selectOne("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?", [table]);
429
+ return row !== null;
430
+ };
431
+ const createTable = async (table, columns, options) => {
432
+ const quotedTable = quoteIdentifier(table, 'table name');
433
+ const entries = Object.entries(columns);
434
+ if (entries.length === 0) {
435
+ throw new Error(`[@webority/mobile-core] Cannot create table ${table} with no columns.`);
436
+ }
437
+ const definitions = entries.map(([column, definition]) => `${quoteIdentifier(column, 'column name')} ${assertColumnDefinition(definition, column)}`);
438
+ const guard = options?.ifNotExists === false ? '' : 'IF NOT EXISTS ';
439
+ await execute(`CREATE TABLE ${guard}${quotedTable} (${definitions.join(', ')})`);
440
+ };
441
+ const dropTable = async (table, options) => {
442
+ const quotedTable = quoteIdentifier(table, 'table name');
443
+ const guard = options?.ifExists === false ? '' : 'IF EXISTS ';
444
+ await execute(`DROP TABLE ${guard}${quotedTable}`);
445
+ };
446
+ const ensureLedger = async () => {
447
+ await execute(`CREATE TABLE IF NOT EXISTS ${escapeIdentifier(MIGRATIONS_TABLE)} (` + 'version INTEGER PRIMARY KEY, name TEXT, applied_at TEXT NOT NULL, checksum TEXT NOT NULL)');
448
+ };
449
+ const recordLedgerSql = `INSERT OR REPLACE INTO ${escapeIdentifier(MIGRATIONS_TABLE)} ` + '(version, name, applied_at, checksum) VALUES (?, ?, ?, ?)';
450
+ const wipe = async () => {
451
+ const pragmaRow = await selectOne('PRAGMA foreign_keys');
452
+ const wasOn = Number(pragmaRow?.foreign_keys ?? 0) === 1;
453
+ // PRAGMA foreign_keys is a documented no-op inside a transaction, so the
454
+ // toggle has to straddle it. Left on, deleting a parent table before its
455
+ // children aborts the run and leaves the database half emptied.
456
+ await execute('PRAGMA foreign_keys = OFF');
457
+ try {
458
+ await transaction(async tx => {
459
+ const tables = await tx.select("SELECT name FROM sqlite_master WHERE type = 'table' " + "AND name NOT LIKE 'sqlite_%' AND name <> ?", [MIGRATIONS_TABLE]);
460
+ for (const table of tables) {
461
+ await tx.execute(`DELETE FROM ${escapeIdentifier(table.name)}`);
462
+ }
463
+ });
464
+ } finally {
465
+ await execute(`PRAGMA foreign_keys = ${wasOn ? 'ON' : 'OFF'}`);
466
+ }
467
+ };
468
+ const migrate = async (migrations, options) => {
469
+ const seenVersions = new Set();
470
+ for (const migration of migrations) {
471
+ if (seenVersions.has(migration.version)) {
472
+ throw new Error(`[@webority/mobile-core] Duplicate migration version ${migration.version} ` + `for database "${name}".`);
473
+ }
474
+ seenVersions.add(migration.version);
475
+ }
476
+ const versionRow = await selectOne('PRAGMA user_version');
477
+ let currentVersion = Number(versionRow?.user_version ?? 0);
478
+ if (migrations.length > 0 && !options?.allowDowngrade) {
479
+ const highest = migrations.reduce((max, m) => Math.max(max, m.version), 0);
480
+ if (currentVersion > highest) {
481
+ throw new Error(`[@webority/mobile-core] Database "${name}" is at schema version ` + `${currentVersion}, which is newer than the highest migration supplied (${highest}). ` + 'This build would run old code against a newer schema. Ship the missing migrations, ' + 'or pass { allowDowngrade: true } if this build is known to be compatible.');
482
+ }
483
+ }
484
+ await ensureLedger();
485
+ const appliedRows = await select(`SELECT version, checksum FROM ${escapeIdentifier(MIGRATIONS_TABLE)}`);
486
+ const appliedChecksums = new Map(appliedRows.map(row => [Number(row.version), String(row.checksum)]));
487
+ for (const migration of migrations) {
488
+ if (migration.version > currentVersion) {
489
+ continue;
490
+ }
491
+ const recorded = appliedChecksums.get(migration.version);
492
+ // A database migrated before the ledger existed has no row to compare
493
+ // against, which is not a mismatch.
494
+ if (recorded === undefined) {
495
+ continue;
496
+ }
497
+ const checksum = migrationChecksum(migration);
498
+ if (recorded !== checksum) {
499
+ throw new Error(`[@webority/mobile-core] Migration ${migration.version} has changed since ` + `it was applied to database "${name}" (recorded ${recorded}, now ${checksum}). ` + 'A released migration must never be edited; add a new one instead.');
500
+ }
501
+ }
502
+ const pending = migrations.filter(migration => migration.version > currentVersion).sort((a, b) => a.version - b.version);
503
+ for (const migration of pending) {
504
+ const checksum = migrationChecksum(migration);
505
+ await transaction(async tx => {
506
+ if (typeof migration.up === 'function') {
507
+ await migration.up(tx);
508
+ } else {
509
+ const statements = Array.isArray(migration.up) ? migration.up : [migration.up];
510
+ for (const statement of statements) {
511
+ await tx.execute(statement);
512
+ }
513
+ }
514
+ // The ledger row is written inside the migration's own transaction so
515
+ // it can never disagree with user_version after a partial failure.
516
+ await tx.execute(recordLedgerSql, [migration.version, migration.name ?? null, new Date().toISOString(), checksum]);
517
+ // PRAGMA statements do not accept bound `?` parameters in SQLite, so the
518
+ // version (a validated integer from this module's own loop, never user
519
+ // input) is inlined rather than passed as a param.
520
+ await tx.execute(`PRAGMA user_version = ${migration.version}`);
521
+ });
522
+ currentVersion = migration.version;
523
+ }
524
+ return currentVersion;
525
+ };
526
+ const reset = async migrations => {
527
+ await wipe();
528
+ await ensureLedger();
529
+ await transaction(async tx => {
530
+ await tx.execute(`DELETE FROM ${escapeIdentifier(MIGRATIONS_TABLE)}`);
531
+ await tx.execute('PRAGMA user_version = 0');
532
+ });
533
+ return migrate(migrations);
534
+ };
535
+ const baseline = async (version, migrations) => {
536
+ assertNonNegativeInteger(version, 'baseline version');
537
+ await ensureLedger();
538
+ const appliedAt = new Date().toISOString();
539
+ await transaction(async tx => {
540
+ for (const migration of migrations ?? []) {
541
+ if (migration.version > version) {
542
+ continue;
543
+ }
544
+ await tx.execute(recordLedgerSql, [migration.version, migration.name ?? null, appliedAt, migrationChecksum(migration)]);
545
+ }
546
+ await tx.execute(`PRAGMA user_version = ${version}`);
547
+ });
548
+ };
549
+ const close = async () => {
550
+ await raw.close();
551
+ };
552
+ return {
553
+ name,
554
+ execute,
555
+ select,
556
+ selectOne,
557
+ transaction,
558
+ insert,
559
+ insertMany,
560
+ update,
561
+ remove,
562
+ upsert,
563
+ findAll,
564
+ findOne,
565
+ count,
566
+ tableExists,
567
+ createTable,
568
+ dropTable,
569
+ wipe,
570
+ reset,
571
+ migrate,
572
+ baseline,
573
+ close
574
+ };
575
+ };
576
+ export const openDatabase = async (name, options) => {
577
+ const driver = requireBackend('open');
578
+ const raw = await driver.open(name, options ? {
579
+ location: options.location
580
+ } : undefined);
581
+ const db = createDb(name, raw);
582
+ if (options?.pragmas !== false) {
583
+ const pragmas = {
584
+ ...DEFAULT_PRAGMAS,
585
+ ...(options?.pragmas ?? {})
586
+ };
587
+ for (const [pragma, value] of Object.entries(pragmas)) {
588
+ assertSqlIdentifier(pragma, 'PRAGMA name');
589
+ await db.execute(`PRAGMA ${pragma} = ${renderPragmaValue(pragma, value)}`);
590
+ }
591
+ }
592
+ return db;
593
+ };
594
+ //# sourceMappingURL=index.js.map