@webority/mobile-core 0.3.0 → 0.4.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/README.md +3 -1
- package/lib/commonjs/push/index.js +1 -1
- package/lib/commonjs/push/push.js +1 -1
- package/lib/commonjs/sqlite/index.js +1 -1
- package/lib/module/push/index.js +1 -1
- package/lib/module/push/push.js +1 -1
- package/lib/module/sqlite/index.js +1 -1
- package/lib/typescript/commonjs/push/index.d.ts +1 -1
- package/lib/typescript/commonjs/push/push.d.ts +15 -3
- package/lib/typescript/commonjs/sqlite/index.d.ts +5 -0
- package/lib/typescript/module/push/index.d.ts +1 -1
- package/lib/typescript/module/push/push.d.ts +15 -3
- package/lib/typescript/module/sqlite/index.d.ts +5 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -73,7 +73,9 @@ error toast middleware skips.
|
|
|
73
73
|
peer, and reports `unavailable` without it or in Expo Go. Call `initPush` once at startup with
|
|
74
74
|
`getInstallationId`, `register`, `unregister` and `onOpen`; `enablePush` asks permission, sends
|
|
75
75
|
the native token (`Fcm` on Android, `Apns` on iOS) and the bundle id to `register`, and
|
|
76
|
-
re-registers when the token changes; `disablePush` calls `unregister
|
|
76
|
+
re-registers when the token changes; `disablePush` calls `unregister` and turns push off only once
|
|
77
|
+
it succeeds (a failure rejects and leaves push on). `resetPush` turns push off on the phone with no
|
|
78
|
+
server call and drops the native token, for a sign-out with no session left. `onOpen` receives the
|
|
77
79
|
tapped notification's data as strings, including the tap that launched the app. `usePush` gives a
|
|
78
80
|
settings switch its `status` and `enabled`. The library does not remember the switch: an app that
|
|
79
81
|
keeps push on calls `enablePush` at each start. `setBadgeCount(count)` sets the app icon badge.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),Object.defineProperty(exports,"PUSH_STATUS",{enumerable:!0,get:function(){return _status.PUSH_STATUS}}),Object.defineProperty(exports,"disablePush",{enumerable:!0,get:function(){return _push.disablePush}}),Object.defineProperty(exports,"enablePush",{enumerable:!0,get:function(){return _push.enablePush}}),Object.defineProperty(exports,"initPush",{enumerable:!0,get:function(){return _push.initPush}}),Object.defineProperty(exports,"notifyLocal",{enumerable:!0,get:function(){return _push.notifyLocal}}),Object.defineProperty(exports,"onAction",{enumerable:!0,get:function(){return _push.onAction}}),Object.defineProperty(exports,"onReceive",{enumerable:!0,get:function(){return _push.onReceive}}),Object.defineProperty(exports,"setBadgeCount",{enumerable:!0,get:function(){return _push.setBadgeCount}}),Object.defineProperty(exports,"setCategories",{enumerable:!0,get:function(){return _push.setCategories}}),Object.defineProperty(exports,"setPushImplementation",{enumerable:!0,get:function(){return _push.setPushImplementation}}),Object.defineProperty(exports,"usePush",{enumerable:!0,get:function(){return _push.usePush}});var _push=require("./push.js"),_status=require("./status.js");
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),Object.defineProperty(exports,"PUSH_STATUS",{enumerable:!0,get:function(){return _status.PUSH_STATUS}}),Object.defineProperty(exports,"disablePush",{enumerable:!0,get:function(){return _push.disablePush}}),Object.defineProperty(exports,"enablePush",{enumerable:!0,get:function(){return _push.enablePush}}),Object.defineProperty(exports,"initPush",{enumerable:!0,get:function(){return _push.initPush}}),Object.defineProperty(exports,"notifyLocal",{enumerable:!0,get:function(){return _push.notifyLocal}}),Object.defineProperty(exports,"onAction",{enumerable:!0,get:function(){return _push.onAction}}),Object.defineProperty(exports,"onReceive",{enumerable:!0,get:function(){return _push.onReceive}}),Object.defineProperty(exports,"resetPush",{enumerable:!0,get:function(){return _push.resetPush}}),Object.defineProperty(exports,"setBadgeCount",{enumerable:!0,get:function(){return _push.setBadgeCount}}),Object.defineProperty(exports,"setCategories",{enumerable:!0,get:function(){return _push.setCategories}}),Object.defineProperty(exports,"setPushImplementation",{enumerable:!0,get:function(){return _push.setPushImplementation}}),Object.defineProperty(exports,"usePush",{enumerable:!0,get:function(){return _push.usePush}});var _push=require("./push.js"),_status=require("./status.js");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.usePush=exports.toPushReceived=exports.toPushAction=exports.toNotificationActions=exports.setPushImplementation=exports.setCategories=exports.setBadgeCount=exports.routeTaskPayload=exports.resolveModules=exports.onReceive=exports.onAction=exports.notifyLocal=exports.initPush=exports.enablePush=exports.disablePush=void 0;var _react=require("react"),_reactNative=require("react-native"),_index=require("../logger/index.js");let resolved,override=null;const setPushImplementation=e=>{override=e};exports.setPushImplementation=setPushImplementation;const resolveModules=()=>{if(override)return override;if(void 0!==resolved)return resolved;try{if(require("expo").isRunningInExpoGo())return resolved=null,resolved;const e=require("expo-notifications"),t=require("expo-application");resolved={notifications:e,applicationId:t.applicationId,getApnsEnvironment:()=>t.getIosPushNotificationServiceEnvironmentAsync()}}catch(e){_index.Logger.warn("[push] expo-notifications is not available; push is off",e),resolved=null}return resolved};exports.resolveModules=resolveModules;let state={status:"unknown",enabled:!1};const subscribers=new Set,setState=e=>{state={...state,...e};for(const e of subscribers)e()};let config=null,channelsReady=Promise.resolve(),tokenSubscription=null,actionHandler=null;const receiveHandlers=new Set;let listening=!1,listeningForReceived=!1;const pendingResponses=[],routedAt=new Map,REPEAT_WINDOW_MS=1e4,requireConfig=()=>{if(!config)throw new Error("[@webority/mobile-core] call initPush before enablePush or disablePush.");return config},toPermissionStatus=e=>e.granted?"granted":"denied"===e.status?"denied":"unknown",toStringData=e=>{const t={};for(const[n,o]of Object.entries(e??{}))null!=o&&(t[n]="object"==typeof o?JSON.stringify(o):String(o));return t},contentData=e=>{const t=e.request.content;if(t.data)return toStringData(t.data);if("string"==typeof t.dataString)try{return toStringData(JSON.parse(t.dataString))}catch(e){_index.Logger.error("[push] notification data is not JSON",e)}return{}},triggerData=e=>{const t=e.request.trigger,n={...t?.remoteMessage?.data??{}};for(const[e,o]of Object.entries(t?.payload??{}))"aps"===e||"body"===e&&"object"==typeof o||(n[e]=o);return toStringData(n)},notificationData=e=>({...triggerData(e),...contentData(e)}),responseData=e=>notificationData(e.notification),toPushReceived=e=>({notificationId:e.request.identifier,title:e.request.content.title??null,body:e.request.content.body??null,data:notificationData(e)});exports.toPushReceived=toPushReceived;const toPushAction=e=>({actionId:e.actionIdentifier,...e.userText?{userText:e.userText}:{},data:responseData(e),notificationId:e.notification.request.identifier});exports.toPushAction=toPushAction;const toNotificationActions=e=>e.map(e=>({identifier:e.id,buttonTitle:e.title,...e.textInput?{textInput:{submitButtonTitle:e.textInput.submitTitle,placeholder:e.textInput.placeholder}}:{},options:{opensAppToForeground:e.opensApp??!0,isDestructive:e.destructive??!1,isAuthenticationRequired:e.authenticationRequired??!1}}));exports.toNotificationActions=toNotificationActions;const routeResponse=async(e,t)=>{const n=`${t.notification.request.identifier}:${t.actionIdentifier}`,o=routedAt.get(n);if(void 0!==o&&Date.now()-o<1e4)return;if(t.actionIdentifier!==e.DEFAULT_ACTION_IDENTIFIER&&actionHandler){routedAt.set(n,Date.now());try{await actionHandler(toPushAction(t))}catch(e){_index.Logger.error("[push] onAction threw",e)}}else if(config){routedAt.set(n,Date.now());try{config.onOpen(responseData(t))}catch(e){_index.Logger.error("[push] onOpen threw",e)}}else pendingResponses.push(t)},drainPending=async e=>{const t=pendingResponses.splice(0);for(const n of t)await routeResponse(e,n)},listenForResponses=e=>{listening||(listening=!0,e.addNotificationResponseReceivedListener(t=>{routeResponse(e,t)}),e.getLastNotificationResponseAsync().then(async t=>(t&&await routeResponse(e,t),e.clearLastNotificationResponseAsync?.())).catch(e=>_index.Logger.error("[push] reading the launch notification failed",e)))},routeTaskPayload=async e=>{const t=resolveModules(),n=e;t&&"string"==typeof n?.actionIdentifier&&"string"==typeof n.notification?.request?.identifier&&await routeResponse(t.notifications,n)};exports.routeTaskPayload=routeTaskPayload;const createChannels=async(e,t)=>{for(const n of t)try{await e.setNotificationChannelAsync(n.id,{name:n.name,importance:"high"===n.importance?e.AndroidImportance.HIGH:e.AndroidImportance.DEFAULT})}catch(e){_index.Logger.error(`[push] creating channel ${n.id} failed`,e)}},APNS_ENVIRONMENTS={development:"Development",production:"Production"},readApnsEnvironment=async e=>{try{return APNS_ENVIRONMENTS[await e.getApnsEnvironment()??""]??null}catch(e){return _index.Logger.warn("[push] reading the APNs environment failed",e),null}},toDevice=async(e,t)=>{const n="android"===e.type?"Fcm":"ios"===e.type?"Apns":null;if(!n||"string"!=typeof e.data)return null;if(!t.applicationId)throw new Error("[@webority/mobile-core] push needs the app bundle id from expo-application.");return{installationId:await requireConfig().getInstallationId(),platform:n,token:e.data,appId:t.applicationId,apnsEnvironment:"Apns"===n?await readApnsEnvironment(t):null}},initPush=e=>{const t=null===config;if(config=e,!t)return;const n=resolveModules();if(!n)return void setState({status:"unavailable"});const o=n.notifications;o.setNotificationHandler({handleNotification:async()=>{const e=config?.showInForeground?.()??!0;return{shouldShowBanner:e,shouldShowList:e,shouldPlaySound:e&&(config?.playSoundInForeground?.()??!0),shouldSetBadge:!0}}}),"android"===_reactNative.Platform.OS&&e.androidChannels?.length&&(channelsReady=createChannels(o,e.androidChannels)),listenForResponses(o),drainPending(o),o.getPermissionsAsync().then(e=>setState({status:toPermissionStatus(e)})).catch(e=>_index.Logger.error("[push] permission check failed",e))};exports.initPush=initPush;let registering=null,registeredKey=null;const registerOnce=e=>{const t=JSON.stringify(e);if(registering?.key===t)return registering.promise;const n=requireConfig().register(e).then(()=>{registeredKey=t}).finally(()=>{registering?.promise===n&&(registering=null)});return registering={key:t,promise:n},n},enablePush=async({prompt:e=!0}={})=>{requireConfig();const t=resolveModules();if(!t)return setState({status:"unavailable"}),"unavailable";const n=t.notifications;await channelsReady;let o=await n.getPermissionsAsync();if(!o.granted&&e&&(o=await n.requestPermissionsAsync()),!o.granted){const e=toPermissionStatus(o);return setState({status:e}),e}const i=await toDevice(await n.getDevicePushTokenAsync(),t);return i?(await registerOnce(i),tokenSubscription?.remove(),tokenSubscription=n.addPushTokenListener(e=>{toDevice(e,t).then(e=>e&&JSON.stringify(e)!==registeredKey?registerOnce(e):void 0).catch(e=>_index.Logger.error("[push] re-registering a new token failed",e))}),setState({status:"granted",enabled:!0}),"granted"):(setState({status:"unavailable"}),"unavailable")};exports.enablePush=enablePush;const
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.usePush=exports.toPushReceived=exports.toPushAction=exports.toNotificationActions=exports.setPushImplementation=exports.setCategories=exports.setBadgeCount=exports.routeTaskPayload=exports.resolveModules=exports.resetPush=exports.onReceive=exports.onAction=exports.notifyLocal=exports.initPush=exports.enablePush=exports.disablePush=void 0;var _react=require("react"),_reactNative=require("react-native"),_index=require("../logger/index.js");let resolved,override=null;const setPushImplementation=e=>{override=e};exports.setPushImplementation=setPushImplementation;const resolveModules=()=>{if(override)return override;if(void 0!==resolved)return resolved;try{if(require("expo").isRunningInExpoGo())return resolved=null,resolved;const e=require("expo-notifications"),t=require("expo-application");resolved={notifications:e,applicationId:t.applicationId,getApnsEnvironment:()=>t.getIosPushNotificationServiceEnvironmentAsync()}}catch(e){_index.Logger.warn("[push] expo-notifications is not available; push is off",e),resolved=null}return resolved};exports.resolveModules=resolveModules;let state={status:"unknown",enabled:!1};const subscribers=new Set,setState=e=>{state={...state,...e};for(const e of subscribers)e()};let config=null,channelsReady=Promise.resolve(),tokenSubscription=null,actionHandler=null;const receiveHandlers=new Set;let listening=!1,listeningForReceived=!1;const pendingResponses=[],routedAt=new Map,REPEAT_WINDOW_MS=1e4,requireConfig=()=>{if(!config)throw new Error("[@webority/mobile-core] call initPush before enablePush or disablePush.");return config},toPermissionStatus=e=>e.granted?"granted":"denied"===e.status?"denied":"unknown",toStringData=e=>{const t={};for(const[n,o]of Object.entries(e??{}))null!=o&&(t[n]="object"==typeof o?JSON.stringify(o):String(o));return t},contentData=e=>{const t=e.request.content;if(t.data)return toStringData(t.data);if("string"==typeof t.dataString)try{return toStringData(JSON.parse(t.dataString))}catch(e){_index.Logger.error("[push] notification data is not JSON",e)}return{}},triggerData=e=>{const t=e.request.trigger,n={...t?.remoteMessage?.data??{}};for(const[e,o]of Object.entries(t?.payload??{}))"aps"===e||"body"===e&&"object"==typeof o||(n[e]=o);return toStringData(n)},notificationData=e=>({...triggerData(e),...contentData(e)}),responseData=e=>notificationData(e.notification),toPushReceived=e=>({notificationId:e.request.identifier,title:e.request.content.title??null,body:e.request.content.body??null,data:notificationData(e)});exports.toPushReceived=toPushReceived;const toPushAction=e=>({actionId:e.actionIdentifier,...e.userText?{userText:e.userText}:{},data:responseData(e),notificationId:e.notification.request.identifier});exports.toPushAction=toPushAction;const toNotificationActions=e=>e.map(e=>({identifier:e.id,buttonTitle:e.title,...e.textInput?{textInput:{submitButtonTitle:e.textInput.submitTitle,placeholder:e.textInput.placeholder}}:{},options:{opensAppToForeground:e.opensApp??!0,isDestructive:e.destructive??!1,isAuthenticationRequired:e.authenticationRequired??!1}}));exports.toNotificationActions=toNotificationActions;const routeResponse=async(e,t)=>{const n=`${t.notification.request.identifier}:${t.actionIdentifier}`,o=routedAt.get(n);if(void 0!==o&&Date.now()-o<1e4)return;if(t.actionIdentifier!==e.DEFAULT_ACTION_IDENTIFIER&&actionHandler){routedAt.set(n,Date.now());try{await actionHandler(toPushAction(t))}catch(e){_index.Logger.error("[push] onAction threw",e)}}else if(config){routedAt.set(n,Date.now());try{config.onOpen(responseData(t))}catch(e){_index.Logger.error("[push] onOpen threw",e)}}else pendingResponses.push(t)},drainPending=async e=>{const t=pendingResponses.splice(0);for(const n of t)await routeResponse(e,n)},listenForResponses=e=>{listening||(listening=!0,e.addNotificationResponseReceivedListener(t=>{routeResponse(e,t)}),e.getLastNotificationResponseAsync().then(async t=>(t&&await routeResponse(e,t),e.clearLastNotificationResponseAsync?.())).catch(e=>_index.Logger.error("[push] reading the launch notification failed",e)))},routeTaskPayload=async e=>{const t=resolveModules(),n=e;t&&"string"==typeof n?.actionIdentifier&&"string"==typeof n.notification?.request?.identifier&&await routeResponse(t.notifications,n)};exports.routeTaskPayload=routeTaskPayload;const createChannels=async(e,t)=>{for(const n of t)try{await e.setNotificationChannelAsync(n.id,{name:n.name,importance:"high"===n.importance?e.AndroidImportance.HIGH:e.AndroidImportance.DEFAULT})}catch(e){_index.Logger.error(`[push] creating channel ${n.id} failed`,e)}},APNS_ENVIRONMENTS={development:"Development",production:"Production"},readApnsEnvironment=async e=>{try{return APNS_ENVIRONMENTS[await e.getApnsEnvironment()??""]??null}catch(e){return _index.Logger.warn("[push] reading the APNs environment failed",e),null}},toDevice=async(e,t)=>{const n="android"===e.type?"Fcm":"ios"===e.type?"Apns":null;if(!n||"string"!=typeof e.data)return null;if(!t.applicationId)throw new Error("[@webority/mobile-core] push needs the app bundle id from expo-application.");return{installationId:await requireConfig().getInstallationId(),platform:n,token:e.data,appId:t.applicationId,apnsEnvironment:"Apns"===n?await readApnsEnvironment(t):null}},initPush=e=>{const t=null===config;if(config=e,!t)return;const n=resolveModules();if(!n)return void setState({status:"unavailable"});const o=n.notifications;o.setNotificationHandler({handleNotification:async()=>{const e=config?.showInForeground?.()??!0;return{shouldShowBanner:e,shouldShowList:e,shouldPlaySound:e&&(config?.playSoundInForeground?.()??!0),shouldSetBadge:!0}}}),"android"===_reactNative.Platform.OS&&e.androidChannels?.length&&(channelsReady=createChannels(o,e.androidChannels)),listenForResponses(o),drainPending(o),o.getPermissionsAsync().then(e=>setState({status:toPermissionStatus(e)})).catch(e=>_index.Logger.error("[push] permission check failed",e))};exports.initPush=initPush;let registering=null,registeredKey=null,unregistering=null,tokenDrop=Promise.resolve();const registerOnce=e=>{const t=JSON.stringify(e);if(registering?.key===t)return registering.promise;const n=requireConfig().register(e).then(()=>{registeredKey=t}).finally(()=>{registering?.promise===n&&(registering=null)});return registering={key:t,promise:n},n},enablePush=async({prompt:e=!0}={})=>{requireConfig();const t=resolveModules();if(!t)return setState({status:"unavailable"}),"unavailable";const n=t.notifications;await channelsReady,await tokenDrop;let o=await n.getPermissionsAsync();if(!o.granted&&e&&(o=await n.requestPermissionsAsync()),!o.granted){const e=toPermissionStatus(o);return setState({status:e}),e}const i=await toDevice(await n.getDevicePushTokenAsync(),t);return i?(await registerOnce(i),tokenSubscription?.remove(),tokenSubscription=n.addPushTokenListener(e=>{(unregistering??Promise.resolve()).then(()=>state.enabled?toDevice(e,t):null).then(e=>e&&JSON.stringify(e)!==registeredKey?registerOnce(e):void 0).catch(e=>_index.Logger.error("[push] re-registering a new token failed",e))}),setState({status:"granted",enabled:!0}),"granted"):(setState({status:"unavailable"}),"unavailable")};exports.enablePush=enablePush;const stopFollowingToken=()=>{tokenSubscription?.remove(),tokenSubscription=null,registeredKey=null,setState({enabled:!1})},disablePush=async()=>{const e=requireConfig(),t=await e.getInstallationId();await(registering?.promise.catch(()=>{}));let settle=()=>{};const n=new Promise(e=>{settle=e});unregistering=n;try{await e.unregister(t),stopFollowingToken()}finally{unregistering===n&&(unregistering=null),settle()}};exports.disablePush=disablePush;const resetPush=async()=>{stopFollowingToken();const e=resolveModules();if(!e)return;const t=e.notifications.unregisterForNotificationsAsync();tokenDrop=t.catch(()=>{}),await t};exports.resetPush=resetPush;const setBadgeCount=async e=>{const t=resolveModules();t&&await t.notifications.setBadgeCountAsync(e)};exports.setBadgeCount=setBadgeCount;const setCategories=async e=>{const t=resolveModules();if(t)for(const n of e)await t.notifications.setNotificationCategoryAsync(n.id,toNotificationActions(n.actions))};exports.setCategories=setCategories;const onAction=e=>{actionHandler=e;const t=resolveModules();return t&&(listenForResponses(t.notifications),drainPending(t.notifications)),()=>{actionHandler===e&&(actionHandler=null)}};exports.onAction=onAction;const onReceive=e=>{receiveHandlers.add(e);const t=resolveModules();return t&&!listeningForReceived&&(listeningForReceived=!0,t.notifications.addNotificationReceivedListener(e=>{const t=toPushReceived(e);for(const e of[...receiveHandlers])try{e(t)}catch(e){_index.Logger.error("[push] onReceive threw",e)}})),()=>{receiveHandlers.delete(e)}};exports.onReceive=onReceive;const notifyLocal=async({title:e,body:t,data:n})=>{const o=resolveModules();o&&await o.notifications.scheduleNotificationAsync({content:{title:e,body:t,data:n??{}},trigger:null})};exports.notifyLocal=notifyLocal;const subscribe=e=>(subscribers.add(e),()=>{subscribers.delete(e)}),getSnapshot=()=>state,enable=async()=>{await enablePush()},usePush=()=>({...(0,_react.useSyncExternalStore)(subscribe,getSnapshot,getSnapshot),enable:enable,disable:disablePush});exports.usePush=usePush;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.whereIn=exports.toSqliteValue=exports.setSqliteImplementation=exports.openDatabase=exports.isSqliteAvailable=exports.fromSqliteBoolean=exports.assertSqlIdentifier=exports.MIGRATIONS_TABLE=exports.DEFAULT_PRAGMAS=void 0;var ExpoSqlite=_interopRequireWildcard(require("expo-sqlite")),_index=require("../logger/index.js"),_expo=require("./adapters/expo.js");function _interopRequireWildcard(e,t){if("function"==typeof WeakMap)var r=new WeakMap,n=new WeakMap;return(_interopRequireWildcard=function(e,t){if(!t&&e&&e.__esModule)return e;var i,o,a={__proto__:null,default:e};if(null===e||"object"!=typeof e&&"function"!=typeof e)return a;if(i=t?n:r){if(i.has(e))return i.get(e);i.set(e,a)}for(const t in e)"default"!==t&&{}.hasOwnProperty.call(e,t)&&((o=(i=Object.defineProperty)&&Object.getOwnPropertyDescriptor(e,t))&&(o.get||o.set)?i(a,t,o):a[t]=e[t]);return a})(e,t)}let expoDriver,backend=null;const setSqliteImplementation=e=>{backend=e};exports.setSqliteImplementation=setSqliteImplementation;const hasNativeSqlite=e=>"function"==typeof e?.openDatabaseAsync,resolveDriver=()=>{if(backend)return backend;if(void 0===expoDriver){const e=ExpoSqlite;expoDriver=hasNativeSqlite(e)?(0,_expo.adaptExpoSqlite)(e):null}return expoDriver},isSqliteAvailable=()=>null!==resolveDriver();exports.isSqliteAvailable=isSqliteAvailable;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 _index.Logger.error(t.message),t}return t},MIGRATIONS_TABLE=exports.MIGRATIONS_TABLE="_webority_migrations",IDENTIFIER=/^[A-Za-z_][A-Za-z0-9_]*$/,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};exports.assertSqlIdentifier=assertSqlIdentifier;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},toSqliteValue=e=>void 0===e?null:"boolean"==typeof e?e?1:0:e instanceof Date?e.toISOString():e;exports.toSqliteValue=toSqliteValue;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};exports.fromSqliteBoolean=fromSqliteBoolean;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)}};exports.whereIn=whereIn;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)},DEFAULT_PRAGMAS=exports.DEFAULT_PRAGMAS={journal_mode:"WAL",synchronous:"NORMAL",foreign_keys:"ON",busy_timeout:5e3},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 _index.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){_index.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)}`),l=new Map(s.map(e=>[Number(e.version),String(e.checksum)]));for(const r of t){if(r.version>a)continue;const t=l.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 c=t.filter(e=>e.version>a).sort((e,t)=>e.version-t.version);for(const e of c){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}`}),l=s.length>0?`DO UPDATE SET ${s.join(", ")}`:"DO NOTHING";await execute(`${n} ON CONFLICT (${a.join(", ")}) ${l}`,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()}}},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};exports.openDatabase=openDatabase;
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.whereIn=exports.toSqliteValue=exports.setSqliteImplementation=exports.openDatabase=exports.isSqliteAvailable=exports.fromSqliteBoolean=exports.assertSqlIdentifier=exports.MIGRATIONS_TABLE=exports.DEFAULT_PRAGMAS=void 0;var ExpoSqlite=_interopRequireWildcard(require("expo-sqlite")),_index=require("../logger/index.js"),_expo=require("./adapters/expo.js");function _interopRequireWildcard(e,t){if("function"==typeof WeakMap)var r=new WeakMap,n=new WeakMap;return(_interopRequireWildcard=function(e,t){if(!t&&e&&e.__esModule)return e;var i,o,a={__proto__:null,default:e};if(null===e||"object"!=typeof e&&"function"!=typeof e)return a;if(i=t?n:r){if(i.has(e))return i.get(e);i.set(e,a)}for(const t in e)"default"!==t&&{}.hasOwnProperty.call(e,t)&&((o=(i=Object.defineProperty)&&Object.getOwnPropertyDescriptor(e,t))&&(o.get||o.set)?i(a,t,o):a[t]=e[t]);return a})(e,t)}let expoDriver,backend=null;const setSqliteImplementation=e=>{backend=e};exports.setSqliteImplementation=setSqliteImplementation;const hasNativeSqlite=e=>"function"==typeof e?.openDatabaseAsync,resolveDriver=()=>{if(backend)return backend;if(void 0===expoDriver){const e=ExpoSqlite;expoDriver=hasNativeSqlite(e)?(0,_expo.adaptExpoSqlite)(e):null}return expoDriver},isSqliteAvailable=()=>null!==resolveDriver();exports.isSqliteAvailable=isSqliteAvailable;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 _index.Logger.error(t.message),t}return t},MIGRATIONS_TABLE=exports.MIGRATIONS_TABLE="_webority_migrations",IDENTIFIER=/^[A-Za-z_][A-Za-z0-9_]*$/,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};exports.assertSqlIdentifier=assertSqlIdentifier;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},toSqliteValue=e=>void 0===e?null:"boolean"==typeof e?e?1:0:e instanceof Date?e.toISOString():e;exports.toSqliteValue=toSqliteValue;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};exports.fromSqliteBoolean=fromSqliteBoolean;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)}};exports.whereIn=whereIn;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)},DEFAULT_PRAGMAS=exports.DEFAULT_PRAGMAS={journal_mode:"WAL",synchronous:"NORMAL",foreign_keys:"ON",busy_timeout:5e3},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},queriesOver=e=>{const select=async(t,r)=>(await e(t,r)).rows;return{execute:e,select:select,selectOne:async(e,t)=>{const r=await select(e,t);return r.length>0?r[0]:null}}},createDb=(e,t)=>{let r=Promise.resolve();const exclusive=e=>{const t=r.then(e);return r=t.catch(()=>{}),t},run=async(e,r)=>{try{return await t.execute(e,r)}catch(t){throw _index.Logger.error(`[@webority/mobile-core] SQLite execute failed: ${e}`,t),t}},n=queriesOver(run),{execute:i,select:o,selectOne:a}=queriesOver((e,t)=>exclusive(()=>run(e,t))),runTransaction=async e=>{let r=!0;const n=queriesOver((e,t)=>r?run(e,t):Promise.reject(new Error("[@webority/mobile-core] A transaction handle was used after its transaction ended. Use the database itself outside transaction().")));try{let r;await t.execute("BEGIN");try{r=await e(n)}catch(e){try{await t.execute("ROLLBACK")}catch(e){_index.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}finally{r=!1}},transaction=e=>exclusive(()=>runTransaction(e)),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),a=[...i.params];let s=`SELECT * FROM ${n}${i.clause}${buildOrderBy(r?.orderBy)}`;return void 0!==r?.limit?(assertNonNegativeInteger(r.limit,"limit"),s+=" LIMIT ?",a.push(r.limit)):void 0!==r?.offset&&(s+=" LIMIT -1"),void 0!==r?.offset&&(assertNonNegativeInteger(r.offset,"offset"),s+=" OFFSET ?",a.push(r.offset)),o(s,a)},ensureLedger=async()=>{await i(`CREATE TABLE IF NOT EXISTS ${escapeIdentifier(MIGRATIONS_TABLE)} (version INTEGER PRIMARY KEY, name TEXT, applied_at TEXT NOT NULL, checksum TEXT NOT NULL)`)},s=`INSERT OR REPLACE INTO ${escapeIdentifier(MIGRATIONS_TABLE)} (version, name, applied_at, checksum) VALUES (?, ?, ?, ?)`,wipe=()=>exclusive(async()=>{const e=await n.selectOne("PRAGMA foreign_keys"),t=1===Number(e?.foreign_keys??0);await run("PRAGMA foreign_keys = OFF");try{await runTransaction(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 run("PRAGMA foreign_keys = "+(t?"ON":"OFF"))}}),migrate=async(t,r)=>{const n=new Set;for(const r of t){if(n.has(r.version))throw new Error(`[@webority/mobile-core] Duplicate migration version ${r.version} for database "${e}".`);n.add(r.version)}const i=await a("PRAGMA user_version");let l=Number(i?.user_version??0);if(t.length>0&&!r?.allowDowngrade){const r=t.reduce((e,t)=>Math.max(e,t.version),0);if(l>r)throw new Error(`[@webority/mobile-core] Database "${e}" is at schema version ${l}, 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 c=await o(`SELECT version, checksum FROM ${escapeIdentifier(MIGRATIONS_TABLE)}`),u=new Map(c.map(e=>[Number(e.version),String(e.checksum)]));for(const r of t){if(r.version>l)continue;const t=u.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 f=t.filter(e=>e.version>l).sort((e,t)=>e.version-t.version);for(const e of f){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(s,[e.version,e.name??null,(new Date).toISOString(),t]),await r.execute(`PRAGMA user_version = ${e.version}`)}),l=e.version}return l};return{name:e,execute:i,select:o,selectOne:a,transaction:transaction,insert:async(e,t)=>{const{sql:r,params:n}=insertSql(e,t);return(await i(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"),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")} = ?`),s=o.map(([,e])=>toSqliteValue(e)),l=buildWhere(r);return(await i(`UPDATE ${n} SET ${a.join(", ")}${l.clause}`,[...s,...l.params])).rowsAffected},remove:async(e,t)=>{const r=quoteIdentifier(e,"table name"),n=buildWhere(t);return(await i(`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:o}=insertSql(e,t),a=new Set(r),s=r.map(e=>quoteIdentifier(e,"column name")),l=Object.keys(t).filter(e=>!a.has(e)).map(e=>{const t=quoteIdentifier(e,"column name");return`${t} = excluded.${t}`}),c=l.length>0?`DO UPDATE SET ${l.join(", ")}`:"DO NOTHING";await i(`${n} ON CONFLICT (${s.join(", ")}) ${c}`,o)},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 a(`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 a("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?",[e])},createTable:async(e,t,r)=>{const n=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")} ${assertColumnDefinition(t,e)}`),s=!1===r?.ifNotExists?"":"IF NOT EXISTS ";await i(`CREATE TABLE ${s}${n} (${a.join(", ")})`)},dropTable:async(e,t)=>{const r=quoteIdentifier(e,"table name"),n=!1===t?.ifExists?"":"IF EXISTS ";await i(`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 n=>{for(const i of t??[])i.version>e||await n.execute(s,[i.version,i.name??null,r,migrationChecksum(i)]);await n.execute(`PRAGMA user_version = ${e}`)})},close:()=>exclusive(()=>t.close())}},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};exports.openDatabase=openDatabase;
|
package/lib/module/push/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";export{disablePush,enablePush,initPush,notifyLocal,onAction,onReceive,setBadgeCount,setCategories,setPushImplementation,usePush}from"./push.js";export{PUSH_STATUS}from"./status.js";
|
|
1
|
+
"use strict";export{disablePush,enablePush,initPush,notifyLocal,onAction,onReceive,resetPush,setBadgeCount,setCategories,setPushImplementation,usePush}from"./push.js";export{PUSH_STATUS}from"./status.js";
|
package/lib/module/push/push.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";import{useSyncExternalStore}from"react";import{Platform}from"react-native";import{Logger}from"../logger/index.js";let resolved,override=null;export const setPushImplementation=e=>{override=e};export const resolveModules=()=>{if(override)return override;if(void 0!==resolved)return resolved;try{if(require("expo").isRunningInExpoGo())return resolved=null,resolved;const e=require("expo-notifications"),t=require("expo-application");resolved={notifications:e,applicationId:t.applicationId,getApnsEnvironment:()=>t.getIosPushNotificationServiceEnvironmentAsync()}}catch(e){Logger.warn("[push] expo-notifications is not available; push is off",e),resolved=null}return resolved};let state={status:"unknown",enabled:!1};const subscribers=new Set,setState=e=>{state={...state,...e};for(const e of subscribers)e()};let config=null,channelsReady=Promise.resolve(),tokenSubscription=null,actionHandler=null;const receiveHandlers=new Set;let listening=!1,listeningForReceived=!1;const pendingResponses=[],routedAt=new Map,REPEAT_WINDOW_MS=1e4,requireConfig=()=>{if(!config)throw new Error("[@webority/mobile-core] call initPush before enablePush or disablePush.");return config},toPermissionStatus=e=>e.granted?"granted":"denied"===e.status?"denied":"unknown",toStringData=e=>{const t={};for(const[n,o]of Object.entries(e??{}))null!=o&&(t[n]="object"==typeof o?JSON.stringify(o):String(o));return t},contentData=e=>{const t=e.request.content;if(t.data)return toStringData(t.data);if("string"==typeof t.dataString)try{return toStringData(JSON.parse(t.dataString))}catch(e){Logger.error("[push] notification data is not JSON",e)}return{}},triggerData=e=>{const t=e.request.trigger,n={...t?.remoteMessage?.data??{}};for(const[e,o]of Object.entries(t?.payload??{}))"aps"===e||"body"===e&&"object"==typeof o||(n[e]=o);return toStringData(n)},notificationData=e=>({...triggerData(e),...contentData(e)}),responseData=e=>notificationData(e.notification);export const toPushReceived=e=>({notificationId:e.request.identifier,title:e.request.content.title??null,body:e.request.content.body??null,data:notificationData(e)});export const toPushAction=e=>({actionId:e.actionIdentifier,...e.userText?{userText:e.userText}:{},data:responseData(e),notificationId:e.notification.request.identifier});export const toNotificationActions=e=>e.map(e=>({identifier:e.id,buttonTitle:e.title,...e.textInput?{textInput:{submitButtonTitle:e.textInput.submitTitle,placeholder:e.textInput.placeholder}}:{},options:{opensAppToForeground:e.opensApp??!0,isDestructive:e.destructive??!1,isAuthenticationRequired:e.authenticationRequired??!1}}));const routeResponse=async(e,t)=>{const n=`${t.notification.request.identifier}:${t.actionIdentifier}`,o=routedAt.get(n);if(void 0!==o&&Date.now()-o<1e4)return;if(t.actionIdentifier!==e.DEFAULT_ACTION_IDENTIFIER&&actionHandler){routedAt.set(n,Date.now());try{await actionHandler(toPushAction(t))}catch(e){Logger.error("[push] onAction threw",e)}}else if(config){routedAt.set(n,Date.now());try{config.onOpen(responseData(t))}catch(e){Logger.error("[push] onOpen threw",e)}}else pendingResponses.push(t)},drainPending=async e=>{const t=pendingResponses.splice(0);for(const n of t)await routeResponse(e,n)},listenForResponses=e=>{listening||(listening=!0,e.addNotificationResponseReceivedListener(t=>{routeResponse(e,t)}),e.getLastNotificationResponseAsync().then(async t=>(t&&await routeResponse(e,t),e.clearLastNotificationResponseAsync?.())).catch(e=>Logger.error("[push] reading the launch notification failed",e)))};export const routeTaskPayload=async e=>{const t=resolveModules(),n=e;t&&"string"==typeof n?.actionIdentifier&&"string"==typeof n.notification?.request?.identifier&&await routeResponse(t.notifications,n)};const createChannels=async(e,t)=>{for(const n of t)try{await e.setNotificationChannelAsync(n.id,{name:n.name,importance:"high"===n.importance?e.AndroidImportance.HIGH:e.AndroidImportance.DEFAULT})}catch(e){Logger.error(`[push] creating channel ${n.id} failed`,e)}},APNS_ENVIRONMENTS={development:"Development",production:"Production"},readApnsEnvironment=async e=>{try{return APNS_ENVIRONMENTS[await e.getApnsEnvironment()??""]??null}catch(e){return Logger.warn("[push] reading the APNs environment failed",e),null}},toDevice=async(e,t)=>{const n="android"===e.type?"Fcm":"ios"===e.type?"Apns":null;if(!n||"string"!=typeof e.data)return null;if(!t.applicationId)throw new Error("[@webority/mobile-core] push needs the app bundle id from expo-application.");return{installationId:await requireConfig().getInstallationId(),platform:n,token:e.data,appId:t.applicationId,apnsEnvironment:"Apns"===n?await readApnsEnvironment(t):null}};export const initPush=e=>{const t=null===config;if(config=e,!t)return;const n=resolveModules();if(!n)return void setState({status:"unavailable"});const o=n.notifications;o.setNotificationHandler({handleNotification:async()=>{const e=config?.showInForeground?.()??!0;return{shouldShowBanner:e,shouldShowList:e,shouldPlaySound:e&&(config?.playSoundInForeground?.()??!0),shouldSetBadge:!0}}}),"android"===Platform.OS&&e.androidChannels?.length&&(channelsReady=createChannels(o,e.androidChannels)),listenForResponses(o),drainPending(o),o.getPermissionsAsync().then(e=>setState({status:toPermissionStatus(e)})).catch(e=>Logger.error("[push] permission check failed",e))};let registering=null,registeredKey=null;const registerOnce=e=>{const t=JSON.stringify(e);if(registering?.key===t)return registering.promise;const n=requireConfig().register(e).then(()=>{registeredKey=t}).finally(()=>{registering?.promise===n&&(registering=null)});return registering={key:t,promise:n},n};export const enablePush=async({prompt:e=!0}={})=>{requireConfig();const t=resolveModules();if(!t)return setState({status:"unavailable"}),"unavailable";const n=t.notifications;await channelsReady;let o=await n.getPermissionsAsync();if(!o.granted&&e&&(o=await n.requestPermissionsAsync()),!o.granted){const e=toPermissionStatus(o);return setState({status:e}),e}const i=await toDevice(await n.getDevicePushTokenAsync(),t);return i?(await registerOnce(i),tokenSubscription?.remove(),tokenSubscription=n.addPushTokenListener(e=>{toDevice(e,t).then(e=>e&&JSON.stringify(e)!==registeredKey?registerOnce(e):void 0).catch(e=>Logger.error("[push] re-registering a new token failed",e))}),setState({status:"granted",enabled:!0}),"granted"):(setState({status:"unavailable"}),"unavailable")};
|
|
1
|
+
"use strict";import{useSyncExternalStore}from"react";import{Platform}from"react-native";import{Logger}from"../logger/index.js";let resolved,override=null;export const setPushImplementation=e=>{override=e};export const resolveModules=()=>{if(override)return override;if(void 0!==resolved)return resolved;try{if(require("expo").isRunningInExpoGo())return resolved=null,resolved;const e=require("expo-notifications"),t=require("expo-application");resolved={notifications:e,applicationId:t.applicationId,getApnsEnvironment:()=>t.getIosPushNotificationServiceEnvironmentAsync()}}catch(e){Logger.warn("[push] expo-notifications is not available; push is off",e),resolved=null}return resolved};let state={status:"unknown",enabled:!1};const subscribers=new Set,setState=e=>{state={...state,...e};for(const e of subscribers)e()};let config=null,channelsReady=Promise.resolve(),tokenSubscription=null,actionHandler=null;const receiveHandlers=new Set;let listening=!1,listeningForReceived=!1;const pendingResponses=[],routedAt=new Map,REPEAT_WINDOW_MS=1e4,requireConfig=()=>{if(!config)throw new Error("[@webority/mobile-core] call initPush before enablePush or disablePush.");return config},toPermissionStatus=e=>e.granted?"granted":"denied"===e.status?"denied":"unknown",toStringData=e=>{const t={};for(const[n,o]of Object.entries(e??{}))null!=o&&(t[n]="object"==typeof o?JSON.stringify(o):String(o));return t},contentData=e=>{const t=e.request.content;if(t.data)return toStringData(t.data);if("string"==typeof t.dataString)try{return toStringData(JSON.parse(t.dataString))}catch(e){Logger.error("[push] notification data is not JSON",e)}return{}},triggerData=e=>{const t=e.request.trigger,n={...t?.remoteMessage?.data??{}};for(const[e,o]of Object.entries(t?.payload??{}))"aps"===e||"body"===e&&"object"==typeof o||(n[e]=o);return toStringData(n)},notificationData=e=>({...triggerData(e),...contentData(e)}),responseData=e=>notificationData(e.notification);export const toPushReceived=e=>({notificationId:e.request.identifier,title:e.request.content.title??null,body:e.request.content.body??null,data:notificationData(e)});export const toPushAction=e=>({actionId:e.actionIdentifier,...e.userText?{userText:e.userText}:{},data:responseData(e),notificationId:e.notification.request.identifier});export const toNotificationActions=e=>e.map(e=>({identifier:e.id,buttonTitle:e.title,...e.textInput?{textInput:{submitButtonTitle:e.textInput.submitTitle,placeholder:e.textInput.placeholder}}:{},options:{opensAppToForeground:e.opensApp??!0,isDestructive:e.destructive??!1,isAuthenticationRequired:e.authenticationRequired??!1}}));const routeResponse=async(e,t)=>{const n=`${t.notification.request.identifier}:${t.actionIdentifier}`,o=routedAt.get(n);if(void 0!==o&&Date.now()-o<1e4)return;if(t.actionIdentifier!==e.DEFAULT_ACTION_IDENTIFIER&&actionHandler){routedAt.set(n,Date.now());try{await actionHandler(toPushAction(t))}catch(e){Logger.error("[push] onAction threw",e)}}else if(config){routedAt.set(n,Date.now());try{config.onOpen(responseData(t))}catch(e){Logger.error("[push] onOpen threw",e)}}else pendingResponses.push(t)},drainPending=async e=>{const t=pendingResponses.splice(0);for(const n of t)await routeResponse(e,n)},listenForResponses=e=>{listening||(listening=!0,e.addNotificationResponseReceivedListener(t=>{routeResponse(e,t)}),e.getLastNotificationResponseAsync().then(async t=>(t&&await routeResponse(e,t),e.clearLastNotificationResponseAsync?.())).catch(e=>Logger.error("[push] reading the launch notification failed",e)))};export const routeTaskPayload=async e=>{const t=resolveModules(),n=e;t&&"string"==typeof n?.actionIdentifier&&"string"==typeof n.notification?.request?.identifier&&await routeResponse(t.notifications,n)};const createChannels=async(e,t)=>{for(const n of t)try{await e.setNotificationChannelAsync(n.id,{name:n.name,importance:"high"===n.importance?e.AndroidImportance.HIGH:e.AndroidImportance.DEFAULT})}catch(e){Logger.error(`[push] creating channel ${n.id} failed`,e)}},APNS_ENVIRONMENTS={development:"Development",production:"Production"},readApnsEnvironment=async e=>{try{return APNS_ENVIRONMENTS[await e.getApnsEnvironment()??""]??null}catch(e){return Logger.warn("[push] reading the APNs environment failed",e),null}},toDevice=async(e,t)=>{const n="android"===e.type?"Fcm":"ios"===e.type?"Apns":null;if(!n||"string"!=typeof e.data)return null;if(!t.applicationId)throw new Error("[@webority/mobile-core] push needs the app bundle id from expo-application.");return{installationId:await requireConfig().getInstallationId(),platform:n,token:e.data,appId:t.applicationId,apnsEnvironment:"Apns"===n?await readApnsEnvironment(t):null}};export const initPush=e=>{const t=null===config;if(config=e,!t)return;const n=resolveModules();if(!n)return void setState({status:"unavailable"});const o=n.notifications;o.setNotificationHandler({handleNotification:async()=>{const e=config?.showInForeground?.()??!0;return{shouldShowBanner:e,shouldShowList:e,shouldPlaySound:e&&(config?.playSoundInForeground?.()??!0),shouldSetBadge:!0}}}),"android"===Platform.OS&&e.androidChannels?.length&&(channelsReady=createChannels(o,e.androidChannels)),listenForResponses(o),drainPending(o),o.getPermissionsAsync().then(e=>setState({status:toPermissionStatus(e)})).catch(e=>Logger.error("[push] permission check failed",e))};let registering=null,registeredKey=null,unregistering=null,tokenDrop=Promise.resolve();const registerOnce=e=>{const t=JSON.stringify(e);if(registering?.key===t)return registering.promise;const n=requireConfig().register(e).then(()=>{registeredKey=t}).finally(()=>{registering?.promise===n&&(registering=null)});return registering={key:t,promise:n},n};export const enablePush=async({prompt:e=!0}={})=>{requireConfig();const t=resolveModules();if(!t)return setState({status:"unavailable"}),"unavailable";const n=t.notifications;await channelsReady,await tokenDrop;let o=await n.getPermissionsAsync();if(!o.granted&&e&&(o=await n.requestPermissionsAsync()),!o.granted){const e=toPermissionStatus(o);return setState({status:e}),e}const i=await toDevice(await n.getDevicePushTokenAsync(),t);return i?(await registerOnce(i),tokenSubscription?.remove(),tokenSubscription=n.addPushTokenListener(e=>{(unregistering??Promise.resolve()).then(()=>state.enabled?toDevice(e,t):null).then(e=>e&&JSON.stringify(e)!==registeredKey?registerOnce(e):void 0).catch(e=>Logger.error("[push] re-registering a new token failed",e))}),setState({status:"granted",enabled:!0}),"granted"):(setState({status:"unavailable"}),"unavailable")};const stopFollowingToken=()=>{tokenSubscription?.remove(),tokenSubscription=null,registeredKey=null,setState({enabled:!1})};export const disablePush=async()=>{const e=requireConfig(),t=await e.getInstallationId();await(registering?.promise.catch(()=>{}));let settle=()=>{};const n=new Promise(e=>{settle=e});unregistering=n;try{await e.unregister(t),stopFollowingToken()}finally{unregistering===n&&(unregistering=null),settle()}};export const resetPush=async()=>{stopFollowingToken();const e=resolveModules();if(!e)return;const t=e.notifications.unregisterForNotificationsAsync();tokenDrop=t.catch(()=>{}),await t};export const setBadgeCount=async e=>{const t=resolveModules();t&&await t.notifications.setBadgeCountAsync(e)};export const setCategories=async e=>{const t=resolveModules();if(t)for(const n of e)await t.notifications.setNotificationCategoryAsync(n.id,toNotificationActions(n.actions))};export const onAction=e=>{actionHandler=e;const t=resolveModules();return t&&(listenForResponses(t.notifications),drainPending(t.notifications)),()=>{actionHandler===e&&(actionHandler=null)}};export const onReceive=e=>{receiveHandlers.add(e);const t=resolveModules();return t&&!listeningForReceived&&(listeningForReceived=!0,t.notifications.addNotificationReceivedListener(e=>{const t=toPushReceived(e);for(const e of[...receiveHandlers])try{e(t)}catch(e){Logger.error("[push] onReceive threw",e)}})),()=>{receiveHandlers.delete(e)}};export const notifyLocal=async({title:e,body:t,data:n})=>{const o=resolveModules();o&&await o.notifications.scheduleNotificationAsync({content:{title:e,body:t,data:n??{}},trigger:null})};const subscribe=e=>(subscribers.add(e),()=>{subscribers.delete(e)}),getSnapshot=()=>state,enable=async()=>{await enablePush()};export const usePush=()=>({...useSyncExternalStore(subscribe,getSnapshot,getSnapshot),enable:enable,disable:disablePush});
|
|
@@ -1 +1 @@
|
|
|
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};
|
|
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},queriesOver=e=>{const select=async(t,r)=>(await e(t,r)).rows;return{execute:e,select:select,selectOne:async(e,t)=>{const r=await select(e,t);return r.length>0?r[0]:null}}},createDb=(e,t)=>{let r=Promise.resolve();const exclusive=e=>{const t=r.then(e);return r=t.catch(()=>{}),t},run=async(e,r)=>{try{return await t.execute(e,r)}catch(t){throw Logger.error(`[@webority/mobile-core] SQLite execute failed: ${e}`,t),t}},n=queriesOver(run),{execute:i,select:o,selectOne:a}=queriesOver((e,t)=>exclusive(()=>run(e,t))),runTransaction=async e=>{let r=!0;const n=queriesOver((e,t)=>r?run(e,t):Promise.reject(new Error("[@webority/mobile-core] A transaction handle was used after its transaction ended. Use the database itself outside transaction().")));try{let r;await t.execute("BEGIN");try{r=await e(n)}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}finally{r=!1}},transaction=e=>exclusive(()=>runTransaction(e)),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),a=[...i.params];let s=`SELECT * FROM ${n}${i.clause}${buildOrderBy(r?.orderBy)}`;return void 0!==r?.limit?(assertNonNegativeInteger(r.limit,"limit"),s+=" LIMIT ?",a.push(r.limit)):void 0!==r?.offset&&(s+=" LIMIT -1"),void 0!==r?.offset&&(assertNonNegativeInteger(r.offset,"offset"),s+=" OFFSET ?",a.push(r.offset)),o(s,a)},ensureLedger=async()=>{await i(`CREATE TABLE IF NOT EXISTS ${escapeIdentifier(MIGRATIONS_TABLE)} (version INTEGER PRIMARY KEY, name TEXT, applied_at TEXT NOT NULL, checksum TEXT NOT NULL)`)},s=`INSERT OR REPLACE INTO ${escapeIdentifier(MIGRATIONS_TABLE)} (version, name, applied_at, checksum) VALUES (?, ?, ?, ?)`,wipe=()=>exclusive(async()=>{const e=await n.selectOne("PRAGMA foreign_keys"),t=1===Number(e?.foreign_keys??0);await run("PRAGMA foreign_keys = OFF");try{await runTransaction(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 run("PRAGMA foreign_keys = "+(t?"ON":"OFF"))}}),migrate=async(t,r)=>{const n=new Set;for(const r of t){if(n.has(r.version))throw new Error(`[@webority/mobile-core] Duplicate migration version ${r.version} for database "${e}".`);n.add(r.version)}const i=await a("PRAGMA user_version");let c=Number(i?.user_version??0);if(t.length>0&&!r?.allowDowngrade){const r=t.reduce((e,t)=>Math.max(e,t.version),0);if(c>r)throw new Error(`[@webority/mobile-core] Database "${e}" is at schema version ${c}, 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 l=await o(`SELECT version, checksum FROM ${escapeIdentifier(MIGRATIONS_TABLE)}`),u=new Map(l.map(e=>[Number(e.version),String(e.checksum)]));for(const r of t){if(r.version>c)continue;const t=u.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 m=t.filter(e=>e.version>c).sort((e,t)=>e.version-t.version);for(const e of m){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(s,[e.version,e.name??null,(new Date).toISOString(),t]),await r.execute(`PRAGMA user_version = ${e.version}`)}),c=e.version}return c};return{name:e,execute:i,select:o,selectOne:a,transaction:transaction,insert:async(e,t)=>{const{sql:r,params:n}=insertSql(e,t);return(await i(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"),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")} = ?`),s=o.map(([,e])=>toSqliteValue(e)),c=buildWhere(r);return(await i(`UPDATE ${n} SET ${a.join(", ")}${c.clause}`,[...s,...c.params])).rowsAffected},remove:async(e,t)=>{const r=quoteIdentifier(e,"table name"),n=buildWhere(t);return(await i(`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:o}=insertSql(e,t),a=new Set(r),s=r.map(e=>quoteIdentifier(e,"column name")),c=Object.keys(t).filter(e=>!a.has(e)).map(e=>{const t=quoteIdentifier(e,"column name");return`${t} = excluded.${t}`}),l=c.length>0?`DO UPDATE SET ${c.join(", ")}`:"DO NOTHING";await i(`${n} ON CONFLICT (${s.join(", ")}) ${l}`,o)},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 a(`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 a("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?",[e])},createTable:async(e,t,r)=>{const n=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")} ${assertColumnDefinition(t,e)}`),s=!1===r?.ifNotExists?"":"IF NOT EXISTS ";await i(`CREATE TABLE ${s}${n} (${a.join(", ")})`)},dropTable:async(e,t)=>{const r=quoteIdentifier(e,"table name"),n=!1===t?.ifExists?"":"IF EXISTS ";await i(`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 n=>{for(const i of t??[])i.version>e||await n.execute(s,[i.version,i.name??null,r,migrationChecksum(i)]);await n.execute(`PRAGMA user_version = ${e}`)})},close:()=>exclusive(()=>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};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type { EnablePushOptions, PushAction, PushActionHandler, PushAndroidChannel, PushApnsEnvironment, PushCategory, PushCategoryAction, PushConfig, PushDevice, PushLocalNotification, PushModules, PushNotificationsLike, PushPlatform, PushReceived, PushReceiveHandler, PushState } from './push';
|
|
2
|
-
export { disablePush, enablePush, initPush, notifyLocal, onAction, onReceive, setBadgeCount, setCategories, setPushImplementation, usePush } from './push';
|
|
2
|
+
export { disablePush, enablePush, initPush, notifyLocal, onAction, onReceive, resetPush, setBadgeCount, setCategories, setPushImplementation, usePush } from './push';
|
|
3
3
|
export type { PushStatus } from './status';
|
|
4
4
|
export { PUSH_STATUS } from './status';
|
|
5
5
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -91,7 +91,7 @@ export interface PushLocalNotification {
|
|
|
91
91
|
}
|
|
92
92
|
export interface PushState {
|
|
93
93
|
status: PushStatus;
|
|
94
|
-
/** True once this process has registered the device, false after `disablePush`. */
|
|
94
|
+
/** True once this process has registered the device, false after `disablePush` or `resetPush`. */
|
|
95
95
|
enabled: boolean;
|
|
96
96
|
}
|
|
97
97
|
interface PermissionResponseLike {
|
|
@@ -148,6 +148,7 @@ export interface PushNotificationsLike {
|
|
|
148
148
|
getPermissionsAsync: () => Promise<PermissionResponseLike>;
|
|
149
149
|
requestPermissionsAsync: () => Promise<PermissionResponseLike>;
|
|
150
150
|
getDevicePushTokenAsync: () => Promise<DevicePushTokenLike>;
|
|
151
|
+
unregisterForNotificationsAsync: () => Promise<void>;
|
|
151
152
|
addPushTokenListener: (listener: (token: DevicePushTokenLike) => void) => SubscriptionLike;
|
|
152
153
|
addNotificationResponseReceivedListener: (listener: (response: NotificationResponseLike) => void) => SubscriptionLike;
|
|
153
154
|
addNotificationReceivedListener: (listener: (notification: NotificationLike) => void) => SubscriptionLike;
|
|
@@ -222,10 +223,21 @@ export declare const initPush: (next: PushConfig) => void;
|
|
|
222
223
|
*/
|
|
223
224
|
export declare const enablePush: ({ prompt }?: EnablePushOptions) => Promise<PushStatus>;
|
|
224
225
|
/**
|
|
225
|
-
* Unregisters the device through `config.unregister
|
|
226
|
-
* changes.
|
|
226
|
+
* Unregisters the device through `config.unregister`, then stops following
|
|
227
|
+
* token changes. When the server call fails it rejects and push stays on, the
|
|
228
|
+
* token listener included, so the app can keep the person's choice and retry.
|
|
229
|
+
* Taps on notifications already delivered still reach `onOpen`.
|
|
227
230
|
*/
|
|
228
231
|
export declare const disablePush: () => Promise<void>;
|
|
232
|
+
/**
|
|
233
|
+
* Turns push off on this phone alone, with no server call: stops following
|
|
234
|
+
* token changes and drops the native device token, so the server's next send to
|
|
235
|
+
* it fails and the server can delete it. For a sign-out where `disablePush`
|
|
236
|
+
* cannot reach the server, or has no session left to authorise the call. Clears
|
|
237
|
+
* the local state at once; the returned promise settles when the token is gone.
|
|
238
|
+
* `enablePush` waits for the drop before it reads a new token.
|
|
239
|
+
*/
|
|
240
|
+
export declare const resetPush: () => Promise<void>;
|
|
229
241
|
/** Sets the app icon badge; a no-op where push is unavailable or the launcher has no badge. */
|
|
230
242
|
export declare const setBadgeCount: (count: number) => Promise<void>;
|
|
231
243
|
/**
|
|
@@ -87,6 +87,11 @@ export interface Db {
|
|
|
87
87
|
execute: (sql: string, params?: readonly unknown[]) => Promise<SqliteResultSet>;
|
|
88
88
|
select: <T = Record<string, unknown>>(sql: string, params?: readonly unknown[]) => Promise<T[]>;
|
|
89
89
|
selectOne: <T = Record<string, unknown>>(sql: string, params?: readonly unknown[]) => Promise<T | null>;
|
|
90
|
+
/**
|
|
91
|
+
* Runs `work` alone on the connection: every other call on this database
|
|
92
|
+
* waits until it commits or rolls back. Inside `work` use only `tx`; a call
|
|
93
|
+
* on the database itself would wait for this transaction and never finish.
|
|
94
|
+
*/
|
|
90
95
|
transaction: <T>(work: (tx: Pick<Db, 'execute' | 'select' | 'selectOne'>) => Promise<T>) => Promise<T>;
|
|
91
96
|
insert: (table: string, row: Record<string, unknown>) => Promise<number | undefined>;
|
|
92
97
|
insertMany: (table: string, rows: Record<string, unknown>[]) => Promise<number>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type { EnablePushOptions, PushAction, PushActionHandler, PushAndroidChannel, PushApnsEnvironment, PushCategory, PushCategoryAction, PushConfig, PushDevice, PushLocalNotification, PushModules, PushNotificationsLike, PushPlatform, PushReceived, PushReceiveHandler, PushState } from './push.js';
|
|
2
|
-
export { disablePush, enablePush, initPush, notifyLocal, onAction, onReceive, setBadgeCount, setCategories, setPushImplementation, usePush } from './push.js';
|
|
2
|
+
export { disablePush, enablePush, initPush, notifyLocal, onAction, onReceive, resetPush, setBadgeCount, setCategories, setPushImplementation, usePush } from './push.js';
|
|
3
3
|
export type { PushStatus } from './status.js';
|
|
4
4
|
export { PUSH_STATUS } from './status.js';
|
|
5
5
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -91,7 +91,7 @@ export interface PushLocalNotification {
|
|
|
91
91
|
}
|
|
92
92
|
export interface PushState {
|
|
93
93
|
status: PushStatus;
|
|
94
|
-
/** True once this process has registered the device, false after `disablePush`. */
|
|
94
|
+
/** True once this process has registered the device, false after `disablePush` or `resetPush`. */
|
|
95
95
|
enabled: boolean;
|
|
96
96
|
}
|
|
97
97
|
interface PermissionResponseLike {
|
|
@@ -148,6 +148,7 @@ export interface PushNotificationsLike {
|
|
|
148
148
|
getPermissionsAsync: () => Promise<PermissionResponseLike>;
|
|
149
149
|
requestPermissionsAsync: () => Promise<PermissionResponseLike>;
|
|
150
150
|
getDevicePushTokenAsync: () => Promise<DevicePushTokenLike>;
|
|
151
|
+
unregisterForNotificationsAsync: () => Promise<void>;
|
|
151
152
|
addPushTokenListener: (listener: (token: DevicePushTokenLike) => void) => SubscriptionLike;
|
|
152
153
|
addNotificationResponseReceivedListener: (listener: (response: NotificationResponseLike) => void) => SubscriptionLike;
|
|
153
154
|
addNotificationReceivedListener: (listener: (notification: NotificationLike) => void) => SubscriptionLike;
|
|
@@ -222,10 +223,21 @@ export declare const initPush: (next: PushConfig) => void;
|
|
|
222
223
|
*/
|
|
223
224
|
export declare const enablePush: ({ prompt }?: EnablePushOptions) => Promise<PushStatus>;
|
|
224
225
|
/**
|
|
225
|
-
* Unregisters the device through `config.unregister
|
|
226
|
-
* changes.
|
|
226
|
+
* Unregisters the device through `config.unregister`, then stops following
|
|
227
|
+
* token changes. When the server call fails it rejects and push stays on, the
|
|
228
|
+
* token listener included, so the app can keep the person's choice and retry.
|
|
229
|
+
* Taps on notifications already delivered still reach `onOpen`.
|
|
227
230
|
*/
|
|
228
231
|
export declare const disablePush: () => Promise<void>;
|
|
232
|
+
/**
|
|
233
|
+
* Turns push off on this phone alone, with no server call: stops following
|
|
234
|
+
* token changes and drops the native device token, so the server's next send to
|
|
235
|
+
* it fails and the server can delete it. For a sign-out where `disablePush`
|
|
236
|
+
* cannot reach the server, or has no session left to authorise the call. Clears
|
|
237
|
+
* the local state at once; the returned promise settles when the token is gone.
|
|
238
|
+
* `enablePush` waits for the drop before it reads a new token.
|
|
239
|
+
*/
|
|
240
|
+
export declare const resetPush: () => Promise<void>;
|
|
229
241
|
/** Sets the app icon badge; a no-op where push is unavailable or the launcher has no badge. */
|
|
230
242
|
export declare const setBadgeCount: (count: number) => Promise<void>;
|
|
231
243
|
/**
|
|
@@ -87,6 +87,11 @@ export interface Db {
|
|
|
87
87
|
execute: (sql: string, params?: readonly unknown[]) => Promise<SqliteResultSet>;
|
|
88
88
|
select: <T = Record<string, unknown>>(sql: string, params?: readonly unknown[]) => Promise<T[]>;
|
|
89
89
|
selectOne: <T = Record<string, unknown>>(sql: string, params?: readonly unknown[]) => Promise<T | null>;
|
|
90
|
+
/**
|
|
91
|
+
* Runs `work` alone on the connection: every other call on this database
|
|
92
|
+
* waits until it commits or rolls back. Inside `work` use only `tx`; a call
|
|
93
|
+
* on the database itself would wait for this transaction and never finish.
|
|
94
|
+
*/
|
|
90
95
|
transaction: <T>(work: (tx: Pick<Db, 'execute' | 'select' | 'selectOne'>) => Promise<T>) => Promise<T>;
|
|
91
96
|
insert: (table: string, row: Record<string, unknown>) => Promise<number | undefined>;
|
|
92
97
|
insertMany: (table: string, rows: Record<string, unknown>[]) => Promise<number>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webority/mobile-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Platform layer for Webority React Native apps: HTTP clients, auth/token storage, config, logging, formatters, validators, network status, permissions, storage and version checks. No UI.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react-native",
|