@webority/mobile-core 0.0.29 → 0.2.0

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 (242) hide show
  1. package/README.md +32 -0
  2. package/lib/commonjs/api/baseUrl.js +1 -34
  3. package/lib/commonjs/api/clientHeaders.js +1 -0
  4. package/lib/commonjs/api/curl.js +1 -109
  5. package/lib/commonjs/api/minAppVersion.js +1 -0
  6. package/lib/commonjs/api/multipart.js +1 -21
  7. package/lib/commonjs/api/publicClient.js +1 -79
  8. package/lib/commonjs/api/requestDefaults.js +1 -0
  9. package/lib/commonjs/api/responseBodyText.js +1 -0
  10. package/lib/commonjs/api/retry.js +1 -91
  11. package/lib/commonjs/api/tokenRefresh.js +1 -205
  12. package/lib/commonjs/api/userClient.js +1 -265
  13. package/lib/commonjs/auth/authStore.js +1 -265
  14. package/lib/commonjs/auth/index.js +1 -44
  15. package/lib/commonjs/auth/jwt.js +1 -112
  16. package/lib/commonjs/config/index.js +1 -72
  17. package/lib/commonjs/deepLink/index.js +1 -286
  18. package/lib/commonjs/download/adapters/expoFs.js +1 -69
  19. package/lib/commonjs/download/adapters/shared.js +1 -13
  20. package/lib/commonjs/download/index.js +1 -250
  21. package/lib/commonjs/errors/extractApiError.js +1 -0
  22. package/lib/commonjs/errors/index.js +1 -0
  23. package/lib/commonjs/formatters/currency.js +1 -70
  24. package/lib/commonjs/formatters/date.js +1 -167
  25. package/lib/commonjs/formatters/dateMath.js +1 -311
  26. package/lib/commonjs/formatters/formatConfig.js +1 -0
  27. package/lib/commonjs/formatters/house.js +1 -0
  28. package/lib/commonjs/formatters/index.js +1 -246
  29. package/lib/commonjs/formatters/initials.js +1 -32
  30. package/lib/commonjs/formatters/intlCache.js +1 -37
  31. package/lib/commonjs/formatters/number.js +1 -89
  32. package/lib/commonjs/formatters/phone.js +1 -369
  33. package/lib/commonjs/formatters/windowsToIanaTable.js +1 -164
  34. package/lib/commonjs/geolocation/adapters/expoLocation.js +1 -126
  35. package/lib/commonjs/geolocation/index.js +1 -355
  36. package/lib/commonjs/index.js +1 -644
  37. package/lib/commonjs/initSDK.js +1 -66
  38. package/lib/commonjs/initUserAuth.js +1 -37
  39. package/lib/commonjs/logger/crashlytics.js +1 -0
  40. package/lib/commonjs/logger/index.js +1 -301
  41. package/lib/commonjs/logger/redact.js +1 -50
  42. package/lib/commonjs/network/index.js +1 -31
  43. package/lib/commonjs/network/networkStatus.js +1 -171
  44. package/lib/commonjs/permissions/calendar.js +1 -14
  45. package/lib/commonjs/permissions/camera.js +1 -14
  46. package/lib/commonjs/permissions/contacts.js +1 -10
  47. package/lib/commonjs/permissions/index.js +1 -133
  48. package/lib/commonjs/permissions/location.js +1 -14
  49. package/lib/commonjs/permissions/mediaLibrary.js +1 -10
  50. package/lib/commonjs/permissions/notifications.js +1 -10
  51. package/lib/commonjs/permissions/photos.js +1 -14
  52. package/lib/commonjs/phone/countries.js +1 -265
  53. package/lib/commonjs/push/index.js +1 -61
  54. package/lib/commonjs/push/push.js +1 -468
  55. package/lib/commonjs/push/status.js +1 -0
  56. package/lib/commonjs/push/task.js +1 -43
  57. package/lib/commonjs/query/apiBaseQuery.js +1 -0
  58. package/lib/commonjs/query/createAppApi.js +1 -0
  59. package/lib/commonjs/query/index.js +1 -0
  60. package/lib/commonjs/query/listeners.js +1 -0
  61. package/lib/commonjs/query/paging.js +1 -0
  62. package/lib/commonjs/query/tags.js +1 -0
  63. package/lib/commonjs/redux/errorToastMiddleware.js +1 -0
  64. package/lib/commonjs/redux/index.js +1 -0
  65. package/lib/commonjs/redux/rootReducer.js +1 -0
  66. package/lib/commonjs/redux/serializableCheck.js +1 -0
  67. package/lib/commonjs/secureScreen/index.js +1 -0
  68. package/lib/commonjs/share/index.js +1 -70
  69. package/lib/commonjs/smsRetriever/index.js +1 -91
  70. package/lib/commonjs/specs/NativeSmsRetriever.js +1 -15
  71. package/lib/commonjs/sqlite/adapters/expo.js +1 -53
  72. package/lib/commonjs/sqlite/index.js +1 -604
  73. package/lib/commonjs/storage/index.js +1 -214
  74. package/lib/commonjs/types/axios.js +1 -4
  75. package/lib/commonjs/types/globals.d.js +1 -6
  76. package/lib/commonjs/utils/base64.js +1 -24
  77. package/lib/commonjs/utils/imageCompression.js +1 -389
  78. package/lib/commonjs/utils/index.js +1 -19
  79. package/lib/commonjs/validators/index.js +1 -227
  80. package/lib/commonjs/versionCheck/buildNumber.js +1 -0
  81. package/lib/commonjs/versionCheck/index.js +1 -419
  82. package/lib/module/api/baseUrl.js +1 -30
  83. package/lib/module/api/clientHeaders.js +1 -0
  84. package/lib/module/api/curl.js +1 -104
  85. package/lib/module/api/minAppVersion.js +1 -0
  86. package/lib/module/api/multipart.js +1 -16
  87. package/lib/module/api/publicClient.js +1 -75
  88. package/lib/module/api/requestDefaults.js +1 -0
  89. package/lib/module/api/responseBodyText.js +1 -0
  90. package/lib/module/api/retry.js +1 -85
  91. package/lib/module/api/tokenRefresh.js +1 -192
  92. package/lib/module/api/userClient.js +1 -243
  93. package/lib/module/auth/authStore.js +1 -257
  94. package/lib/module/auth/index.js +1 -5
  95. package/lib/module/auth/jwt.js +1 -106
  96. package/lib/module/config/index.js +1 -64
  97. package/lib/module/deepLink/index.js +1 -280
  98. package/lib/module/download/adapters/expoFs.js +1 -64
  99. package/lib/module/download/adapters/shared.js +1 -8
  100. package/lib/module/download/index.js +1 -242
  101. package/lib/module/errors/extractApiError.js +1 -0
  102. package/lib/module/errors/index.js +1 -0
  103. package/lib/module/formatters/currency.js +1 -65
  104. package/lib/module/formatters/date.js +1 -159
  105. package/lib/module/formatters/dateMath.js +1 -295
  106. package/lib/module/formatters/formatConfig.js +1 -0
  107. package/lib/module/formatters/house.js +1 -0
  108. package/lib/module/formatters/index.js +1 -9
  109. package/lib/module/formatters/initials.js +1 -27
  110. package/lib/module/formatters/intlCache.js +1 -30
  111. package/lib/module/formatters/number.js +1 -82
  112. package/lib/module/formatters/phone.js +1 -302
  113. package/lib/module/formatters/windowsToIanaTable.js +1 -160
  114. package/lib/module/geolocation/adapters/expoLocation.js +1 -121
  115. package/lib/module/geolocation/index.js +1 -347
  116. package/lib/module/index.js +1 -57
  117. package/lib/module/initSDK.js +1 -61
  118. package/lib/module/initUserAuth.js +1 -33
  119. package/lib/module/logger/crashlytics.js +1 -0
  120. package/lib/module/logger/index.js +1 -295
  121. package/lib/module/logger/redact.js +1 -44
  122. package/lib/module/network/index.js +1 -4
  123. package/lib/module/network/networkStatus.js +1 -161
  124. package/lib/module/permissions/calendar.js +1 -15
  125. package/lib/module/permissions/camera.js +1 -15
  126. package/lib/module/permissions/contacts.js +1 -11
  127. package/lib/module/permissions/index.js +1 -128
  128. package/lib/module/permissions/location.js +1 -15
  129. package/lib/module/permissions/mediaLibrary.js +1 -11
  130. package/lib/module/permissions/notifications.js +1 -11
  131. package/lib/module/permissions/photos.js +1 -15
  132. package/lib/module/phone/countries.js +1 -261
  133. package/lib/module/push/index.js +1 -4
  134. package/lib/module/push/push.js +1 -452
  135. package/lib/module/push/status.js +1 -0
  136. package/lib/module/push/task.js +1 -38
  137. package/lib/module/query/apiBaseQuery.js +1 -0
  138. package/lib/module/query/createAppApi.js +1 -0
  139. package/lib/module/query/index.js +1 -0
  140. package/lib/module/query/listeners.js +1 -0
  141. package/lib/module/query/paging.js +1 -0
  142. package/lib/module/query/tags.js +1 -0
  143. package/lib/module/redux/errorToastMiddleware.js +1 -0
  144. package/lib/module/redux/index.js +1 -0
  145. package/lib/module/redux/rootReducer.js +1 -0
  146. package/lib/module/redux/serializableCheck.js +1 -0
  147. package/lib/module/secureScreen/index.js +1 -0
  148. package/lib/module/share/index.js +1 -64
  149. package/lib/module/smsRetriever/index.js +1 -86
  150. package/lib/module/specs/NativeSmsRetriever.js +1 -13
  151. package/lib/module/sqlite/adapters/expo.js +1 -48
  152. package/lib/module/sqlite/index.js +1 -594
  153. package/lib/module/storage/index.js +1 -208
  154. package/lib/module/types/axios.js +1 -4
  155. package/lib/module/types/globals.d.js +1 -10
  156. package/lib/module/utils/base64.js +1 -19
  157. package/lib/module/utils/imageCompression.js +1 -383
  158. package/lib/module/utils/index.js +1 -4
  159. package/lib/module/validators/index.js +1 -205
  160. package/lib/module/versionCheck/buildNumber.js +1 -0
  161. package/lib/module/versionCheck/index.js +1 -413
  162. package/lib/typescript/commonjs/api/clientHeaders.d.ts +25 -0
  163. package/lib/typescript/commonjs/api/minAppVersion.d.ts +24 -0
  164. package/lib/typescript/commonjs/api/requestDefaults.d.ts +21 -0
  165. package/lib/typescript/commonjs/api/responseBodyText.d.ts +6 -0
  166. package/lib/typescript/commonjs/api/retry.d.ts +23 -9
  167. package/lib/typescript/commonjs/api/tokenRefresh.d.ts +14 -2
  168. package/lib/typescript/commonjs/api/userClient.d.ts +1 -1
  169. package/lib/typescript/commonjs/auth/authStore.d.ts +76 -0
  170. package/lib/typescript/commonjs/auth/index.d.ts +2 -2
  171. package/lib/typescript/commonjs/config/index.d.ts +31 -2
  172. package/lib/typescript/commonjs/errors/extractApiError.d.ts +69 -0
  173. package/lib/typescript/commonjs/errors/index.d.ts +3 -0
  174. package/lib/typescript/commonjs/formatters/currency.d.ts +22 -7
  175. package/lib/typescript/commonjs/formatters/date.d.ts +13 -13
  176. package/lib/typescript/commonjs/formatters/dateMath.d.ts +4 -2
  177. package/lib/typescript/commonjs/formatters/formatConfig.d.ts +48 -0
  178. package/lib/typescript/commonjs/formatters/house.d.ts +143 -0
  179. package/lib/typescript/commonjs/formatters/index.d.ts +4 -0
  180. package/lib/typescript/commonjs/formatters/number.d.ts +15 -5
  181. package/lib/typescript/commonjs/index.d.ts +26 -8
  182. package/lib/typescript/commonjs/initSDK.d.ts +18 -6
  183. package/lib/typescript/commonjs/initUserAuth.d.ts +29 -0
  184. package/lib/typescript/commonjs/logger/crashlytics.d.ts +31 -0
  185. package/lib/typescript/commonjs/logger/index.d.ts +17 -0
  186. package/lib/typescript/commonjs/push/index.d.ts +3 -1
  187. package/lib/typescript/commonjs/push/push.d.ts +1 -1
  188. package/lib/typescript/commonjs/push/status.d.ts +9 -0
  189. package/lib/typescript/commonjs/query/apiBaseQuery.d.ts +55 -0
  190. package/lib/typescript/commonjs/query/createAppApi.d.ts +21 -0
  191. package/lib/typescript/commonjs/query/index.d.ts +9 -0
  192. package/lib/typescript/commonjs/query/listeners.d.ts +11 -0
  193. package/lib/typescript/commonjs/query/paging.d.ts +42 -0
  194. package/lib/typescript/commonjs/query/tags.d.ts +31 -0
  195. package/lib/typescript/commonjs/redux/errorToastMiddleware.d.ts +51 -0
  196. package/lib/typescript/commonjs/redux/index.d.ts +6 -0
  197. package/lib/typescript/commonjs/redux/rootReducer.d.ts +19 -0
  198. package/lib/typescript/commonjs/redux/serializableCheck.d.ts +14 -0
  199. package/lib/typescript/commonjs/secureScreen/index.d.ts +25 -0
  200. package/lib/typescript/commonjs/versionCheck/buildNumber.d.ts +14 -0
  201. package/lib/typescript/commonjs/versionCheck/index.d.ts +28 -17
  202. package/lib/typescript/module/api/clientHeaders.d.ts +25 -0
  203. package/lib/typescript/module/api/minAppVersion.d.ts +24 -0
  204. package/lib/typescript/module/api/requestDefaults.d.ts +21 -0
  205. package/lib/typescript/module/api/responseBodyText.d.ts +6 -0
  206. package/lib/typescript/module/api/retry.d.ts +23 -9
  207. package/lib/typescript/module/api/tokenRefresh.d.ts +14 -2
  208. package/lib/typescript/module/api/userClient.d.ts +1 -1
  209. package/lib/typescript/module/auth/authStore.d.ts +76 -0
  210. package/lib/typescript/module/auth/index.d.ts +2 -2
  211. package/lib/typescript/module/config/index.d.ts +31 -2
  212. package/lib/typescript/module/errors/extractApiError.d.ts +69 -0
  213. package/lib/typescript/module/errors/index.d.ts +3 -0
  214. package/lib/typescript/module/formatters/currency.d.ts +22 -7
  215. package/lib/typescript/module/formatters/date.d.ts +13 -13
  216. package/lib/typescript/module/formatters/dateMath.d.ts +4 -2
  217. package/lib/typescript/module/formatters/formatConfig.d.ts +48 -0
  218. package/lib/typescript/module/formatters/house.d.ts +143 -0
  219. package/lib/typescript/module/formatters/index.d.ts +4 -0
  220. package/lib/typescript/module/formatters/number.d.ts +15 -5
  221. package/lib/typescript/module/index.d.ts +26 -8
  222. package/lib/typescript/module/initSDK.d.ts +18 -6
  223. package/lib/typescript/module/initUserAuth.d.ts +29 -0
  224. package/lib/typescript/module/logger/crashlytics.d.ts +31 -0
  225. package/lib/typescript/module/logger/index.d.ts +17 -0
  226. package/lib/typescript/module/push/index.d.ts +3 -1
  227. package/lib/typescript/module/push/push.d.ts +1 -1
  228. package/lib/typescript/module/push/status.d.ts +9 -0
  229. package/lib/typescript/module/query/apiBaseQuery.d.ts +55 -0
  230. package/lib/typescript/module/query/createAppApi.d.ts +21 -0
  231. package/lib/typescript/module/query/index.d.ts +9 -0
  232. package/lib/typescript/module/query/listeners.d.ts +11 -0
  233. package/lib/typescript/module/query/paging.d.ts +42 -0
  234. package/lib/typescript/module/query/tags.d.ts +31 -0
  235. package/lib/typescript/module/redux/errorToastMiddleware.d.ts +51 -0
  236. package/lib/typescript/module/redux/index.d.ts +6 -0
  237. package/lib/typescript/module/redux/rootReducer.d.ts +19 -0
  238. package/lib/typescript/module/redux/serializableCheck.d.ts +14 -0
  239. package/lib/typescript/module/secureScreen/index.d.ts +25 -0
  240. package/lib/typescript/module/versionCheck/buildNumber.d.ts +14 -0
  241. package/lib/typescript/module/versionCheck/index.d.ts +28 -17
  242. package/package.json +52 -5
@@ -1,594 +1 @@
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
1
+ "use strict";import*as ExpoSqlite from"expo-sqlite";import{Logger}from"../logger/index.js";import{adaptExpoSqlite}from"./adapters/expo.js";let expoDriver,backend=null;export const setSqliteImplementation=e=>{backend=e};const hasNativeSqlite=e=>"function"==typeof e?.openDatabaseAsync,resolveDriver=()=>{if(backend)return backend;if(void 0===expoDriver){const e=ExpoSqlite;expoDriver=hasNativeSqlite(e)?adaptExpoSqlite(e):null}return expoDriver};export const isSqliteAvailable=()=>null!==resolveDriver();const requireBackend=e=>{const t=resolveDriver();if(!t){const t=new Error(`[@webority/mobile-core] No SQLite driver available; cannot ${e}. Install expo-sqlite (the only sanctioned driver) in the consuming app.`);throw Logger.error(t.message),t}return t};export const MIGRATIONS_TABLE="_webority_migrations";const IDENTIFIER=/^[A-Za-z_][A-Za-z0-9_]*$/;export const assertSqlIdentifier=(e,t="identifier")=>{if("string"!=typeof e||!IDENTIFIER.test(e))throw new Error(`[@webority/mobile-core] Invalid SQL ${t} ${JSON.stringify(e)}. 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.`);return e};const quoteIdentifier=(e,t="identifier")=>`"${assertSqlIdentifier(e,t)}"`,escapeIdentifier=e=>`"${e.replace(/"/g,'""')}"`,COLUMN_DEFINITION=/^[A-Za-z0-9_ ,.()'"+*/=<>|%:$-]+$/,assertColumnDefinition=(e,t)=>{const r="string"==typeof e?e.trim():"";if(0===r.length||!COLUMN_DEFINITION.test(r)||r.includes("--"))throw new Error(`[@webority/mobile-core] Invalid column definition for "${t}": ${JSON.stringify(e)}. A definition is raw SQL such as 'INTEGER NOT NULL DEFAULT 0' and may not contain a statement separator or a comment.`);return r};export const toSqliteValue=e=>void 0===e?null:"boolean"==typeof e?e?1:0:e instanceof Date?e.toISOString():e;export const fromSqliteBoolean=e=>{if("boolean"==typeof e)return e;if("number"==typeof e)return 0!==e;if("string"==typeof e){const t=e.trim().toLowerCase();return"1"===t||"true"===t}return!1};export const whereIn=(e,t)=>{const r=quoteIdentifier(e,"column name");return 0===t.length?{sql:"1 = 0",params:[]}:{sql:`${r} IN (${t.map(()=>"?").join(", ")})`,params:t.map(toSqliteValue)}};const isRawWhere=e=>"string"==typeof e.sql&&Object.keys(e).every(e=>"sql"===e||"params"===e),buildWhere=e=>{if(!e)return{clause:"",params:[]};if(isRawWhere(e)){const t=e.sql.trim();return 0===t.length?{clause:"",params:[]}:{clause:` WHERE ${t}`,params:[...e.params??[]]}}const t=Object.entries(e);if(0===t.length)return{clause:"",params:[]};const r=[],n=[];for(const[e,i]of t){const t=quoteIdentifier(e,"column name"),o=toSqliteValue(i);null===o?r.push(`${t} IS NULL`):(r.push(`${t} = ?`),n.push(o))}return{clause:` WHERE ${r.join(" AND ")}`,params:n}},buildOrderBy=e=>{if(void 0===e)return"";const t=e.split(",").map(e=>e.trim()).filter(e=>e.length>0);if(0===t.length)return"";return` ORDER BY ${t.map(t=>{const[r,n,...i]=t.split(/\s+/);if(i.length>0)throw new Error(`[@webority/mobile-core] Invalid orderBy term ${JSON.stringify(t)}. Expected a column name optionally followed by ASC or DESC.`);const o=quoteIdentifier(r,"orderBy column");if(void 0===n)return o;const a=n.toUpperCase();if("ASC"!==a&&"DESC"!==a)throw new Error(`[@webority/mobile-core] Invalid sort direction ${JSON.stringify(n)} in orderBy ${JSON.stringify(e)}. Expected ASC or DESC.`);return`${o} ${a}`}).join(", ")}`},assertNonNegativeInteger=(e,t)=>{if(!Number.isInteger(e)||e<0)throw new Error(`[@webority/mobile-core] Invalid ${t} ${JSON.stringify(e)}. Expected a non-negative integer.`);return e},hashString=e=>{let t=2166136261;for(let r=0;r<e.length;r+=1)t^=e.charCodeAt(r),t=Math.imul(t,16777619)>>>0;return`${t.toString(16).padStart(8,"0")}-${e.length.toString(16)}`},migrationChecksum=e=>{if("function"==typeof e.up)return hashString("fn");const t=Array.isArray(e.up)?e.up.join("\n"):e.up;return hashString(t)};export const DEFAULT_PRAGMAS={journal_mode:"WAL",synchronous:"NORMAL",foreign_keys:"ON",busy_timeout:5e3};const PRAGMA_VALUE=/^[A-Za-z0-9_-]+$/,renderPragmaValue=(e,t)=>{if("number"==typeof t){if(!Number.isFinite(t))throw new Error(`[@webority/mobile-core] Invalid value for PRAGMA ${e}: ${t}.`);return String(t)}if(!PRAGMA_VALUE.test(t))throw new Error(`[@webority/mobile-core] Invalid value ${JSON.stringify(t)} for PRAGMA ${e}. PRAGMA values cannot be bound, so only letters, digits, underscores and hyphens are accepted.`);return t},createDb=(e,t)=>{let r=Promise.resolve();const execute=async(e,r)=>{try{return await t.execute(e,r)}catch(t){throw Logger.error(`[@webority/mobile-core] SQLite execute failed: ${e}`,t),t}},select=async(e,t)=>(await execute(e,t)).rows,selectOne=async(e,t)=>{const r=await select(e,t);return r.length>0?r[0]:null},runTransaction=async e=>{let r;await t.execute("BEGIN");try{r=await e({execute:execute,select:select,selectOne:selectOne})}catch(e){try{await t.execute("ROLLBACK")}catch(e){Logger.error("[@webority/mobile-core] SQLite ROLLBACK failed",e)}throw e}try{await t.execute("COMMIT")}catch(e){try{await t.execute("ROLLBACK")}catch{}throw e}return r},transaction=e=>{const t=r.then(()=>runTransaction(e),()=>runTransaction(e));return r=t.catch(()=>{}),t},insertSql=(e,t)=>{const r=quoteIdentifier(e,"table name"),n=Object.entries(t);if(0===n.length)throw new Error(`[@webority/mobile-core] Cannot insert an empty row into ${e}. Pass at least one column.`);const i=n.map(([e])=>quoteIdentifier(e,"column name")),o=n.map(()=>"?");return{sql:`INSERT INTO ${r} (${i.join(", ")}) VALUES (${o.join(", ")})`,params:n.map(([,e])=>toSqliteValue(e))}},findAll=async(e,t,r)=>{const n=quoteIdentifier(e,"table name"),i=buildWhere(t),o=[...i.params];let a=`SELECT * FROM ${n}${i.clause}${buildOrderBy(r?.orderBy)}`;return void 0!==r?.limit?(assertNonNegativeInteger(r.limit,"limit"),a+=" LIMIT ?",o.push(r.limit)):void 0!==r?.offset&&(a+=" LIMIT -1"),void 0!==r?.offset&&(assertNonNegativeInteger(r.offset,"offset"),a+=" OFFSET ?",o.push(r.offset)),select(a,o)},ensureLedger=async()=>{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)`)},n=`INSERT OR REPLACE INTO ${escapeIdentifier(MIGRATIONS_TABLE)} (version, name, applied_at, checksum) VALUES (?, ?, ?, ?)`,wipe=async()=>{const e=await selectOne("PRAGMA foreign_keys"),t=1===Number(e?.foreign_keys??0);await execute("PRAGMA foreign_keys = OFF");try{await transaction(async e=>{const t=await e.select("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND name <> ?",[MIGRATIONS_TABLE]);for(const r of t)await e.execute(`DELETE FROM ${escapeIdentifier(r.name)}`)})}finally{await execute("PRAGMA foreign_keys = "+(t?"ON":"OFF"))}},migrate=async(t,r)=>{const i=new Set;for(const r of t){if(i.has(r.version))throw new Error(`[@webority/mobile-core] Duplicate migration version ${r.version} for database "${e}".`);i.add(r.version)}const o=await selectOne("PRAGMA user_version");let a=Number(o?.user_version??0);if(t.length>0&&!r?.allowDowngrade){const r=t.reduce((e,t)=>Math.max(e,t.version),0);if(a>r)throw new Error(`[@webority/mobile-core] Database "${e}" is at schema version ${a}, which is newer than the highest migration supplied (${r}). 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.`)}await ensureLedger();const s=await select(`SELECT version, checksum FROM ${escapeIdentifier(MIGRATIONS_TABLE)}`),c=new Map(s.map(e=>[Number(e.version),String(e.checksum)]));for(const r of t){if(r.version>a)continue;const t=c.get(r.version);if(void 0===t)continue;const n=migrationChecksum(r);if(t!==n)throw new Error(`[@webority/mobile-core] Migration ${r.version} has changed since it was applied to database "${e}" (recorded ${t}, now ${n}). A released migration must never be edited; add a new one instead.`)}const l=t.filter(e=>e.version>a).sort((e,t)=>e.version-t.version);for(const e of l){const t=migrationChecksum(e);await transaction(async r=>{if("function"==typeof e.up)await e.up(r);else{const t=Array.isArray(e.up)?e.up:[e.up];for(const e of t)await r.execute(e)}await r.execute(n,[e.version,e.name??null,(new Date).toISOString(),t]),await r.execute(`PRAGMA user_version = ${e.version}`)}),a=e.version}return a};return{name:e,execute:execute,select:select,selectOne:selectOne,transaction:transaction,insert:async(e,t)=>{const{sql:r,params:n}=insertSql(e,t);return(await execute(r,n)).insertId},insertMany:async(e,t)=>{if(0===t.length)return 0;const r=t.map(t=>insertSql(e,t));return await transaction(async e=>{for(const t of r)await e.execute(t.sql,t.params)}),t.length},update:async(e,t,r)=>{const n=quoteIdentifier(e,"table name"),i=Object.entries(t);if(0===i.length)throw new Error(`[@webority/mobile-core] Cannot update ${e} with no values. Pass at least one column to set.`);const o=i.map(([e])=>`${quoteIdentifier(e,"column name")} = ?`),a=i.map(([,e])=>toSqliteValue(e)),s=buildWhere(r);return(await execute(`UPDATE ${n} SET ${o.join(", ")}${s.clause}`,[...a,...s.params])).rowsAffected},remove:async(e,t)=>{const r=quoteIdentifier(e,"table name"),n=buildWhere(t);return(await execute(`DELETE FROM ${r}${n.clause}`,n.params)).rowsAffected},upsert:async(e,t,r)=>{if(0===r.length)throw new Error(`[@webority/mobile-core] upsert on ${e} needs at least one conflict column; without one SQLite has no key to match an existing row on.`);const{sql:n,params:i}=insertSql(e,t),o=new Set(r),a=r.map(e=>quoteIdentifier(e,"column name")),s=Object.keys(t).filter(e=>!o.has(e)).map(e=>{const t=quoteIdentifier(e,"column name");return`${t} = excluded.${t}`}),c=s.length>0?`DO UPDATE SET ${s.join(", ")}`:"DO NOTHING";await execute(`${n} ON CONFLICT (${a.join(", ")}) ${c}`,i)},findAll:findAll,findOne:async(e,t)=>{const r=await findAll(e,t,{limit:1});return r.length>0?r[0]:null},count:async(e,t)=>{const r=quoteIdentifier(e,"table name"),n=buildWhere(t),i=await selectOne(`SELECT COUNT(*) AS cnt FROM ${r}${n.clause}`,n.params);return Number(i?.cnt??0)},tableExists:async e=>{assertSqlIdentifier(e,"table name");return null!==await selectOne("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?",[e])},createTable:async(e,t,r)=>{const n=quoteIdentifier(e,"table name"),i=Object.entries(t);if(0===i.length)throw new Error(`[@webority/mobile-core] Cannot create table ${e} with no columns.`);const o=i.map(([e,t])=>`${quoteIdentifier(e,"column name")} ${assertColumnDefinition(t,e)}`),a=!1===r?.ifNotExists?"":"IF NOT EXISTS ";await execute(`CREATE TABLE ${a}${n} (${o.join(", ")})`)},dropTable:async(e,t)=>{const r=quoteIdentifier(e,"table name"),n=!1===t?.ifExists?"":"IF EXISTS ";await execute(`DROP TABLE ${n}${r}`)},wipe:wipe,reset:async e=>(await wipe(),await ensureLedger(),await transaction(async e=>{await e.execute(`DELETE FROM ${escapeIdentifier(MIGRATIONS_TABLE)}`),await e.execute("PRAGMA user_version = 0")}),migrate(e)),migrate:migrate,baseline:async(e,t)=>{assertNonNegativeInteger(e,"baseline version"),await ensureLedger();const r=(new Date).toISOString();await transaction(async i=>{for(const o of t??[])o.version>e||await i.execute(n,[o.version,o.name??null,r,migrationChecksum(o)]);await i.execute(`PRAGMA user_version = ${e}`)})},close:async()=>{await t.close()}}};export const openDatabase=async(e,t)=>{const r=requireBackend("open"),n=await r.open(e,t?{location:t.location}:void 0),i=createDb(e,n);if(!1!==t?.pragmas){const e={...DEFAULT_PRAGMAS,...t?.pragmas??{}};for(const[t,r]of Object.entries(e))assertSqlIdentifier(t,"PRAGMA name"),await i.execute(`PRAGMA ${t} = ${renderPragmaValue(t,r)}`)}return i};