@webority/mobile-core 0.0.29 → 0.1.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.
- package/lib/commonjs/api/baseUrl.js +1 -34
- package/lib/commonjs/api/curl.js +1 -109
- package/lib/commonjs/api/multipart.js +1 -21
- package/lib/commonjs/api/publicClient.js +1 -79
- package/lib/commonjs/api/responseBodyText.js +1 -0
- package/lib/commonjs/api/retry.js +1 -91
- package/lib/commonjs/api/tokenRefresh.js +1 -205
- package/lib/commonjs/api/userClient.js +1 -265
- package/lib/commonjs/auth/authStore.js +1 -265
- package/lib/commonjs/auth/index.js +1 -44
- package/lib/commonjs/auth/jwt.js +1 -112
- package/lib/commonjs/config/index.js +1 -72
- package/lib/commonjs/deepLink/index.js +1 -286
- package/lib/commonjs/download/adapters/expoFs.js +1 -69
- package/lib/commonjs/download/adapters/shared.js +1 -13
- package/lib/commonjs/download/index.js +1 -250
- package/lib/commonjs/formatters/currency.js +1 -70
- package/lib/commonjs/formatters/date.js +1 -167
- package/lib/commonjs/formatters/dateMath.js +1 -311
- package/lib/commonjs/formatters/index.js +1 -246
- package/lib/commonjs/formatters/initials.js +1 -32
- package/lib/commonjs/formatters/intlCache.js +1 -37
- package/lib/commonjs/formatters/number.js +1 -89
- package/lib/commonjs/formatters/phone.js +1 -369
- package/lib/commonjs/formatters/windowsToIanaTable.js +1 -164
- package/lib/commonjs/geolocation/adapters/expoLocation.js +1 -126
- package/lib/commonjs/geolocation/index.js +1 -355
- package/lib/commonjs/index.js +1 -644
- package/lib/commonjs/initSDK.js +1 -66
- package/lib/commonjs/initUserAuth.js +1 -37
- package/lib/commonjs/logger/crashlytics.js +1 -0
- package/lib/commonjs/logger/index.js +1 -301
- package/lib/commonjs/logger/redact.js +1 -50
- package/lib/commonjs/network/index.js +1 -31
- package/lib/commonjs/network/networkStatus.js +1 -171
- package/lib/commonjs/permissions/calendar.js +1 -14
- package/lib/commonjs/permissions/camera.js +1 -14
- package/lib/commonjs/permissions/contacts.js +1 -10
- package/lib/commonjs/permissions/index.js +1 -133
- package/lib/commonjs/permissions/location.js +1 -14
- package/lib/commonjs/permissions/mediaLibrary.js +1 -10
- package/lib/commonjs/permissions/notifications.js +1 -10
- package/lib/commonjs/permissions/photos.js +1 -14
- package/lib/commonjs/phone/countries.js +1 -265
- package/lib/commonjs/push/index.js +1 -61
- package/lib/commonjs/push/push.js +1 -468
- package/lib/commonjs/push/status.js +1 -0
- package/lib/commonjs/push/task.js +1 -43
- package/lib/commonjs/secureScreen/index.js +1 -0
- package/lib/commonjs/share/index.js +1 -70
- package/lib/commonjs/smsRetriever/index.js +1 -91
- package/lib/commonjs/specs/NativeSmsRetriever.js +1 -15
- package/lib/commonjs/sqlite/adapters/expo.js +1 -53
- package/lib/commonjs/sqlite/index.js +1 -604
- package/lib/commonjs/storage/index.js +1 -214
- package/lib/commonjs/types/axios.js +1 -4
- package/lib/commonjs/types/globals.d.js +1 -6
- package/lib/commonjs/utils/base64.js +1 -24
- package/lib/commonjs/utils/imageCompression.js +1 -389
- package/lib/commonjs/utils/index.js +1 -19
- package/lib/commonjs/validators/index.js +1 -227
- package/lib/commonjs/versionCheck/index.js +1 -419
- package/lib/module/api/baseUrl.js +1 -30
- package/lib/module/api/curl.js +1 -104
- package/lib/module/api/multipart.js +1 -16
- package/lib/module/api/publicClient.js +1 -75
- package/lib/module/api/responseBodyText.js +1 -0
- package/lib/module/api/retry.js +1 -85
- package/lib/module/api/tokenRefresh.js +1 -192
- package/lib/module/api/userClient.js +1 -243
- package/lib/module/auth/authStore.js +1 -257
- package/lib/module/auth/index.js +1 -5
- package/lib/module/auth/jwt.js +1 -106
- package/lib/module/config/index.js +1 -64
- package/lib/module/deepLink/index.js +1 -280
- package/lib/module/download/adapters/expoFs.js +1 -64
- package/lib/module/download/adapters/shared.js +1 -8
- package/lib/module/download/index.js +1 -242
- package/lib/module/formatters/currency.js +1 -65
- package/lib/module/formatters/date.js +1 -159
- package/lib/module/formatters/dateMath.js +1 -295
- package/lib/module/formatters/index.js +1 -9
- package/lib/module/formatters/initials.js +1 -27
- package/lib/module/formatters/intlCache.js +1 -30
- package/lib/module/formatters/number.js +1 -82
- package/lib/module/formatters/phone.js +1 -302
- package/lib/module/formatters/windowsToIanaTable.js +1 -160
- package/lib/module/geolocation/adapters/expoLocation.js +1 -121
- package/lib/module/geolocation/index.js +1 -347
- package/lib/module/index.js +1 -57
- package/lib/module/initSDK.js +1 -61
- package/lib/module/initUserAuth.js +1 -33
- package/lib/module/logger/crashlytics.js +1 -0
- package/lib/module/logger/index.js +1 -295
- package/lib/module/logger/redact.js +1 -44
- package/lib/module/network/index.js +1 -4
- package/lib/module/network/networkStatus.js +1 -161
- package/lib/module/permissions/calendar.js +1 -15
- package/lib/module/permissions/camera.js +1 -15
- package/lib/module/permissions/contacts.js +1 -11
- package/lib/module/permissions/index.js +1 -128
- package/lib/module/permissions/location.js +1 -15
- package/lib/module/permissions/mediaLibrary.js +1 -11
- package/lib/module/permissions/notifications.js +1 -11
- package/lib/module/permissions/photos.js +1 -15
- package/lib/module/phone/countries.js +1 -261
- package/lib/module/push/index.js +1 -4
- package/lib/module/push/push.js +1 -452
- package/lib/module/push/status.js +1 -0
- package/lib/module/push/task.js +1 -38
- package/lib/module/secureScreen/index.js +1 -0
- package/lib/module/share/index.js +1 -64
- package/lib/module/smsRetriever/index.js +1 -86
- package/lib/module/specs/NativeSmsRetriever.js +1 -13
- package/lib/module/sqlite/adapters/expo.js +1 -48
- package/lib/module/sqlite/index.js +1 -594
- package/lib/module/storage/index.js +1 -208
- package/lib/module/types/axios.js +1 -4
- package/lib/module/types/globals.d.js +1 -10
- package/lib/module/utils/base64.js +1 -19
- package/lib/module/utils/imageCompression.js +1 -383
- package/lib/module/utils/index.js +1 -4
- package/lib/module/validators/index.js +1 -205
- package/lib/module/versionCheck/index.js +1 -413
- package/lib/typescript/commonjs/api/responseBodyText.d.ts +6 -0
- package/lib/typescript/commonjs/index.d.ts +5 -0
- package/lib/typescript/commonjs/logger/crashlytics.d.ts +31 -0
- package/lib/typescript/commonjs/push/index.d.ts +3 -1
- package/lib/typescript/commonjs/push/push.d.ts +1 -1
- package/lib/typescript/commonjs/push/status.d.ts +9 -0
- package/lib/typescript/commonjs/secureScreen/index.d.ts +25 -0
- package/lib/typescript/module/api/responseBodyText.d.ts +6 -0
- package/lib/typescript/module/index.d.ts +5 -0
- package/lib/typescript/module/logger/crashlytics.d.ts +31 -0
- package/lib/typescript/module/push/index.d.ts +3 -1
- package/lib/typescript/module/push/push.d.ts +1 -1
- package/lib/typescript/module/push/status.d.ts +9 -0
- package/lib/typescript/module/secureScreen/index.d.ts +25 -0
- package/package.json +23 -4
|
@@ -1,48 +1 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* expo-sqlite. Shape verified against 57.0.2 rather than assumed, and it is the
|
|
5
|
-
* one driver that cannot share an adapter with the others: expo splits reads
|
|
6
|
-
* from writes. `getAllAsync` yields rows but no change count, `runAsync` yields
|
|
7
|
-
* `{ changes, lastInsertRowId }` but no rows, and SqliteResultSet needs all
|
|
8
|
-
* three from one call. `prepareAsync` + `executeAsync` is the only call that
|
|
9
|
-
* returns both, so every statement goes through it rather than being routed by
|
|
10
|
-
* sniffing the SQL for a leading SELECT.
|
|
11
|
-
*
|
|
12
|
-
* The REQUIRE lives in the driver entry point beside this file, never here.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
export const adaptExpoSqlite = mod => ({
|
|
16
|
-
// expo takes the directory as a third positional argument rather than an
|
|
17
|
-
// option, so `location` cannot be passed through the options object.
|
|
18
|
-
open: async (name, options) => {
|
|
19
|
-
const db = await mod.openDatabaseAsync(name, undefined, options?.location);
|
|
20
|
-
return {
|
|
21
|
-
execute: async (sql, params) => {
|
|
22
|
-
const statement = await db.prepareAsync(sql);
|
|
23
|
-
try {
|
|
24
|
-
const result = await statement.executeAsync(params ? [...params] : []);
|
|
25
|
-
// Safe on a write: expo returns [] when the statement produced no
|
|
26
|
-
// first row rather than stepping the statement a second time.
|
|
27
|
-
const rows = await result.getAllAsync();
|
|
28
|
-
return {
|
|
29
|
-
rows,
|
|
30
|
-
rowsAffected: result.changes,
|
|
31
|
-
// expo always reports lastInsertRowId, so unlike the other drivers
|
|
32
|
-
// this is never undefined. It is the connection's last insert rowid,
|
|
33
|
-
// which is only meaningful immediately after an INSERT.
|
|
34
|
-
insertId: result.lastInsertRowId
|
|
35
|
-
};
|
|
36
|
-
} finally {
|
|
37
|
-
// A statement left unfinalised holds a native handle for the life of
|
|
38
|
-
// the connection, and the core executes one per call.
|
|
39
|
-
await statement.finalizeAsync();
|
|
40
|
-
}
|
|
41
|
-
},
|
|
42
|
-
close: async () => {
|
|
43
|
-
await db.closeAsync();
|
|
44
|
-
}
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
//# sourceMappingURL=expo.js.map
|
|
1
|
+
export const adaptExpoSqlite=a=>({open:async(t,e)=>{const n=await a.openDatabaseAsync(t,void 0,e?.location);return{execute:async(a,t)=>{const e=await n.prepareAsync(a);try{const a=await e.executeAsync(t?[...t]:[]);return{rows:await a.getAllAsync(),rowsAffected:a.changes,insertId:a.lastInsertRowId}}finally{await e.finalizeAsync()}},close:async()=>{await n.closeAsync()}}}});
|
|
@@ -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
|
+
import*as e from"expo-sqlite";import{Logger as t}from"../logger/index.js";import{adaptExpoSqlite as n}from"./adapters/expo.js";let r,o=null;export const setSqliteImplementation=e=>{o=e};const resolveDriver=()=>{if(o)return o;if(void 0===r){const t=e;r=(e=>"function"==typeof e?.openDatabaseAsync)(t)?n(t):null}return r};export const isSqliteAvailable=()=>null!==resolveDriver();export const MIGRATIONS_TABLE="_webority_migrations";const a=/^[A-Za-z_][A-Za-z0-9_]*$/;export const assertSqlIdentifier=(e,t="identifier")=>{if("string"!=typeof e||!a.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,'""')}"`,i=/^[A-Za-z0-9_ ,.()'"+*/=<>|%:$-]+$/;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 n=quoteIdentifier(e,"column name");return 0===t.length?{sql:"1 = 0",params:[]}:{sql:`${n} IN (${t.map(()=>"?").join(", ")})`,params:t.map(toSqliteValue)}};const buildWhere=e=>{if(!e)return{clause:"",params:[]};if((e=>"string"==typeof e.sql&&Object.keys(e).every(e=>"sql"===e||"params"===e))(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 n=[],r=[];for(const[e,o]of t){const t=quoteIdentifier(e,"column name"),a=toSqliteValue(o);null===a?n.push(`${t} IS NULL`):(n.push(`${t} = ?`),r.push(a))}return{clause:` WHERE ${n.join(" AND ")}`,params:r}},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 n=0;n<e.length;n+=1)t^=e.charCodeAt(n),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 s=/^[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(!s.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,n)=>{let r=Promise.resolve();const execute=async(e,r)=>{try{return await n.execute(e,r)}catch(n){throw t.error(`[@webority/mobile-core] SQLite execute failed: ${e}`,n),n}},select=async(e,t)=>(await execute(e,t)).rows,selectOne=async(e,t)=>{const n=await select(e,t);return n.length>0?n[0]:null},runTransaction=async e=>{let r;await n.execute("BEGIN");try{r=await e({execute:execute,select:select,selectOne:selectOne})}catch(e){try{await n.execute("ROLLBACK")}catch(e){t.error("[@webority/mobile-core] SQLite ROLLBACK failed",e)}throw e}try{await n.execute("COMMIT")}catch(e){try{await n.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 n=quoteIdentifier(e,"table name"),r=Object.entries(t);if(0===r.length)throw new Error(`[@webority/mobile-core] Cannot insert an empty row into ${e}. Pass at least one column.`);const o=r.map(([e])=>quoteIdentifier(e,"column name")),a=r.map(()=>"?");return{sql:`INSERT INTO ${n} (${o.join(", ")}) VALUES (${a.join(", ")})`,params:r.map(([,e])=>toSqliteValue(e))}},findAll=async(e,t,n)=>{const r=quoteIdentifier(e,"table name"),o=buildWhere(t),a=[...o.params];let i=`SELECT * FROM ${r}${o.clause}${(e=>{if(void 0===e)return"";const t=e.split(",").map(e=>e.trim()).filter(e=>e.length>0);return 0===t.length?"":` ORDER BY ${t.map(t=>{const[n,r,...o]=t.split(/\s+/);if(o.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 a=quoteIdentifier(n,"orderBy column");if(void 0===r)return a;const i=r.toUpperCase();if("ASC"!==i&&"DESC"!==i)throw new Error(`[@webority/mobile-core] Invalid sort direction ${JSON.stringify(r)} in orderBy ${JSON.stringify(e)}. Expected ASC or DESC.`);return`${a} ${i}`}).join(", ")}`})(n?.orderBy)}`;return void 0!==n?.limit?(assertNonNegativeInteger(n.limit,"limit"),i+=" LIMIT ?",a.push(n.limit)):void 0!==n?.offset&&(i+=" LIMIT -1"),void 0!==n?.offset&&(assertNonNegativeInteger(n.offset,"offset"),i+=" OFFSET ?",a.push(n.offset)),select(i,a)},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)`)},o=`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 n of t)await e.execute(`DELETE FROM ${escapeIdentifier(n.name)}`)})}finally{await execute("PRAGMA foreign_keys = "+(t?"ON":"OFF"))}},migrate=async(t,n)=>{const r=new Set;for(const n of t){if(r.has(n.version))throw new Error(`[@webority/mobile-core] Duplicate migration version ${n.version} for database "${e}".`);r.add(n.version)}const a=await selectOne("PRAGMA user_version");let i=Number(a?.user_version??0);if(t.length>0&&!n?.allowDowngrade){const n=t.reduce((e,t)=>Math.max(e,t.version),0);if(i>n)throw new Error(`[@webority/mobile-core] Database "${e}" is at schema version ${i}, which is newer than the highest migration supplied (${n}). 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 n of t){if(n.version>i)continue;const t=c.get(n.version);if(void 0===t)continue;const r=migrationChecksum(n);if(t!==r)throw new Error(`[@webority/mobile-core] Migration ${n.version} has changed since it was applied to database "${e}" (recorded ${t}, now ${r}). A released migration must never be edited; add a new one instead.`)}const l=t.filter(e=>e.version>i).sort((e,t)=>e.version-t.version);for(const e of l){const t=migrationChecksum(e);await transaction(async n=>{if("function"==typeof e.up)await e.up(n);else{const t=Array.isArray(e.up)?e.up:[e.up];for(const e of t)await n.execute(e)}await n.execute(o,[e.version,e.name??null,(new Date).toISOString(),t]),await n.execute(`PRAGMA user_version = ${e.version}`)}),i=e.version}return i};return{name:e,execute:execute,select:select,selectOne:selectOne,transaction:transaction,insert:async(e,t)=>{const{sql:n,params:r}=insertSql(e,t);return(await execute(n,r)).insertId},insertMany:async(e,t)=>{if(0===t.length)return 0;const n=t.map(t=>insertSql(e,t));return await transaction(async e=>{for(const t of n)await e.execute(t.sql,t.params)}),t.length},update:async(e,t,n)=>{const r=quoteIdentifier(e,"table name"),o=Object.entries(t);if(0===o.length)throw new Error(`[@webority/mobile-core] Cannot update ${e} with no values. Pass at least one column to set.`);const a=o.map(([e])=>`${quoteIdentifier(e,"column name")} = ?`),i=o.map(([,e])=>toSqliteValue(e)),s=buildWhere(n);return(await execute(`UPDATE ${r} SET ${a.join(", ")}${s.clause}`,[...i,...s.params])).rowsAffected},remove:async(e,t)=>{const n=quoteIdentifier(e,"table name"),r=buildWhere(t);return(await execute(`DELETE FROM ${n}${r.clause}`,r.params)).rowsAffected},upsert:async(e,t,n)=>{if(0===n.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:r,params:o}=insertSql(e,t),a=new Set(n),i=n.map(e=>quoteIdentifier(e,"column name")),s=Object.keys(t).filter(e=>!a.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(`${r} ON CONFLICT (${i.join(", ")}) ${c}`,o)},findAll:findAll,findOne:async(e,t)=>{const n=await findAll(e,t,{limit:1});return n.length>0?n[0]:null},count:async(e,t)=>{const n=quoteIdentifier(e,"table name"),r=buildWhere(t),o=await selectOne(`SELECT COUNT(*) AS cnt FROM ${n}${r.clause}`,r.params);return Number(o?.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,n)=>{const r=quoteIdentifier(e,"table name"),o=Object.entries(t);if(0===o.length)throw new Error(`[@webority/mobile-core] Cannot create table ${e} with no columns.`);const a=o.map(([e,t])=>`${quoteIdentifier(e,"column name")} ${((e,t)=>{const n="string"==typeof e?e.trim():"";if(0===n.length||!i.test(n)||n.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 n})(t,e)}`),s=!1===n?.ifNotExists?"":"IF NOT EXISTS ";await execute(`CREATE TABLE ${s}${r} (${a.join(", ")})`)},dropTable:async(e,t)=>{const n=quoteIdentifier(e,"table name"),r=!1===t?.ifExists?"":"IF EXISTS ";await execute(`DROP TABLE ${r}${n}`)},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 n=(new Date).toISOString();await transaction(async r=>{for(const a of t??[])a.version>e||await r.execute(o,[a.version,a.name??null,n,migrationChecksum(a)]);await r.execute(`PRAGMA user_version = ${e}`)})},close:async()=>{await n.close()}}};export const openDatabase=async(e,n)=>{const r=(e=>{const n=resolveDriver();if(!n){const n=new Error(`[@webority/mobile-core] No SQLite driver available; cannot ${e}. Install expo-sqlite (the only sanctioned driver) in the consuming app.`);throw t.error(n.message),n}return n})("open"),o=await r.open(e,n?{location:n.location}:void 0),a=createDb(e,o);if(!1!==n?.pragmas){const e={...DEFAULT_PRAGMAS,...n?.pragmas??{}};for(const[t,n]of Object.entries(e))assertSqlIdentifier(t,"PRAGMA name"),await a.execute(`PRAGMA ${t} = ${renderPragmaValue(t,n)}`)}return a};
|