diff --git a/.changeset/khaki-apples-hunt.md b/.changeset/khaki-apples-hunt.md
new file mode 100644
index 00000000..f870da7e
--- /dev/null
+++ b/.changeset/khaki-apples-hunt.md
@@ -0,0 +1,6 @@
+---
+"@powersync/common": minor
+"@powersync/react": minor
+---
+
+Allow compilable queries to be used as hook arguments
diff --git a/demos/django-react-native-todolist/library/widgets/HeaderWidget.tsx b/demos/django-react-native-todolist/library/widgets/HeaderWidget.tsx
index 0dff32a2..f139be2a 100644
--- a/demos/django-react-native-todolist/library/widgets/HeaderWidget.tsx
+++ b/demos/django-react-native-todolist/library/widgets/HeaderWidget.tsx
@@ -39,8 +39,7 @@ export const HeaderWidget: React.FC<{
onPress={() => {
Alert.alert(
'Status',
- `${status.connected ? 'Connected' : 'Disconnected'}. \nLast Synced at ${
- status.lastSyncedAt?.toISOString() ?? '-'
+ `${status.connected ? 'Connected' : 'Disconnected'}. \nLast Synced at ${status.lastSyncedAt?.toISOString() ?? '-'
}\nVersion: ${powersync.sdkVersion}`
);
}}
diff --git a/demos/react-native-supabase-group-chat/src/app/(app)/contacts/index.tsx b/demos/react-native-supabase-group-chat/src/app/(app)/contacts/index.tsx
index 9cdee5ff..6d476a3c 100644
--- a/demos/react-native-supabase-group-chat/src/app/(app)/contacts/index.tsx
+++ b/demos/react-native-supabase-group-chat/src/app/(app)/contacts/index.tsx
@@ -112,7 +112,7 @@ export default function ContactsIndex() {
icon={}
backgroundColor="$brand1"
borderRadius="$3"
- // circular
+ // circular
/>
diff --git a/packages/common/package.json b/packages/common/package.json
index 1359fb26..d29f3cfb 100644
--- a/packages/common/package.json
+++ b/packages/common/package.json
@@ -23,7 +23,8 @@
"homepage": "https://docs.powersync.com/resources/api-reference",
"scripts": {
"build": "tsc -b",
- "clean": "rm -rf lib tsconfig.tsbuildinfo"
+ "clean": "rm -rf lib tsconfig.tsbuildinfo",
+ "test": "vitest"
},
"dependencies": {
"async-mutex": "^0.4.0",
@@ -37,6 +38,7 @@
"@types/lodash": "^4.14.197",
"@types/node": "^20.5.9",
"@types/uuid": "^9.0.1",
- "typescript": "^5.1.3"
+ "typescript": "^5.1.3",
+ "vitest": "^1.5.2"
}
}
diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts
index b40d1cdb..db32a6a8 100644
--- a/packages/common/src/index.ts
+++ b/packages/common/src/index.ts
@@ -30,3 +30,6 @@ export * from './db/schema/TableV2';
export * from './utils/AbortOperation';
export * from './utils/BaseObserver';
export * from './utils/strings';
+export * from './utils/parseQuery';
+
+export * from './types/types';
diff --git a/packages/common/src/types/types.ts b/packages/common/src/types/types.ts
new file mode 100644
index 00000000..b7f48db5
--- /dev/null
+++ b/packages/common/src/types/types.ts
@@ -0,0 +1,9 @@
+export interface CompilableQuery {
+ execute(): Promise;
+ compile(): CompiledQuery;
+}
+
+export interface CompiledQuery {
+ readonly sql: string;
+ readonly parameters: ReadonlyArray;
+}
diff --git a/packages/common/src/utils/parseQuery.ts b/packages/common/src/utils/parseQuery.ts
new file mode 100644
index 00000000..8981cae1
--- /dev/null
+++ b/packages/common/src/utils/parseQuery.ts
@@ -0,0 +1,25 @@
+import type { CompilableQuery } from '../types/types';
+
+export interface ParsedQuery {
+ sqlStatement: string;
+ parameters: any[];
+}
+
+export const parseQuery = (query: string | CompilableQuery, parameters: any[]): ParsedQuery => {
+ let sqlStatement: string;
+
+ if (typeof query == 'string') {
+ sqlStatement = query;
+ } else {
+ const hasAdditionalParameters = parameters.length > 0;
+ if (hasAdditionalParameters) {
+ throw new Error('You cannot pass parameters to a compiled query.');
+ }
+
+ const compiled = query.compile();
+ sqlStatement = compiled.sql;
+ parameters = compiled.parameters as any[];
+ }
+
+ return { sqlStatement, parameters: parameters };
+};
diff --git a/packages/common/tests/tsconfig.json b/packages/common/tests/tsconfig.json
new file mode 100644
index 00000000..41af59e8
--- /dev/null
+++ b/packages/common/tests/tsconfig.json
@@ -0,0 +1,22 @@
+{
+ "compilerOptions": {
+ "baseUrl": "./",
+ "esModuleInterop": true,
+ "jsx": "react",
+ "rootDir": "../",
+ "composite": true,
+ "outDir": "./lib",
+ "lib": ["esnext", "DOM"],
+ "module": "esnext",
+ "sourceMap": true,
+ "moduleResolution": "node",
+ "noFallthroughCasesInSwitch": true,
+ "noImplicitReturns": true,
+ "noImplicitUseStrict": false,
+ "noStrictGenericChecks": false,
+ "resolveJsonModule": true,
+ "skipLibCheck": true,
+ "target": "esnext"
+ },
+ "include": ["../src/**/*"]
+}
diff --git a/packages/common/tests/utils/parseQuery.test.ts b/packages/common/tests/utils/parseQuery.test.ts
new file mode 100644
index 00000000..52b97698
--- /dev/null
+++ b/packages/common/tests/utils/parseQuery.test.ts
@@ -0,0 +1,36 @@
+import { describe, expect, it } from 'vitest';
+import * as SUT from '../../src/utils/parseQuery';
+
+describe('parseQuery', () => {
+ it('should do nothing if the query is a string', () => {
+ const query = 'SELECT * FROM table';
+ const parameters = ['one'];
+ const result = SUT.parseQuery(query, parameters);
+
+ expect(result).toEqual({ sqlStatement: query, parameters: ['one'] });
+ });
+
+ it('should compile the query and return the sql statement and parameters if the query is compilable', () => {
+ const sqlStatement = 'SELECT * FROM table';
+ const parameters = [];
+ const query = {
+ compile: () => ({ sql: sqlStatement, parameters: ['test'] }),
+ execute: () => Promise.resolve([])
+ };
+ const result = SUT.parseQuery(query, parameters);
+
+ expect(result).toEqual({ sqlStatement, parameters: ['test'] });
+ });
+
+ it('should throw an error if there is an additional parameter included in a compiled query', () => {
+ const sqlStatement = 'SELECT * FROM table';
+ const parameters = ['additional parameter'];
+ const query = {
+ compile: () => ({ sql: sqlStatement, parameters: ['test'] }),
+ execute: () => Promise.resolve([])
+ };
+ const result = () => SUT.parseQuery(query, parameters);
+
+ expect(result).toThrowError('You cannot pass parameters to a compiled query.');
+ });
+});
diff --git a/packages/react/README.md b/packages/react/README.md
index b2e9096b..0519057d 100644
--- a/packages/react/README.md
+++ b/packages/react/README.md
@@ -63,14 +63,14 @@ const Component = () => {
### Queries
-Queries will automatically update when a dependant table is updated unless you set the `runQueryOnce` flag.
+Queries will automatically update when a dependant table is updated unless you set the `runQueryOnce` flag. You are also able to use a compilable query (e.g. [Kysely queries](https://github.com/powersync-ja/powersync-js/tree/main/packages/kysely-driver)) as a query argument in place of a SQL statement string.
```JSX
// TodoListDisplay.jsx
import { useQuery } from "@powersync/react";
export const TodoListDisplay = () => {
- const { data: todoLists } = useQuery('SELECT * from lists');
+ const { data: todoLists } = useQuery('SELECT * FROM lists WHERE id = ?', ['id-1'], {runQueryOnce: false});
return
{todoLists.map((l) => (
diff --git a/packages/react/src/hooks/useQuery.ts b/packages/react/src/hooks/useQuery.ts
index 706b61d0..234a0c8a 100644
--- a/packages/react/src/hooks/useQuery.ts
+++ b/packages/react/src/hooks/useQuery.ts
@@ -1,8 +1,8 @@
-import { SQLWatchOptions } from '@powersync/common';
+import { type SQLWatchOptions, parseQuery, type CompilableQuery, type ParsedQuery } from '@powersync/common';
import React from 'react';
import { usePowerSync } from './PowerSyncContext';
-interface AdditionalOptions extends Omit {
+export interface AdditionalOptions extends Omit {
runQueryOnce?: boolean;
}
@@ -37,7 +37,7 @@ export type QueryResult = {
* }
*/
export const useQuery = (
- sqlStatement: string,
+ query: string | CompilableQuery,
parameters: any[] = [],
options: AdditionalOptions = { runQueryOnce: false }
): QueryResult => {
@@ -46,13 +46,23 @@ export const useQuery = (
return { isLoading: false, isFetching: false, data: [], error: new Error('PowerSync not configured.') };
}
+ let parsedQuery: ParsedQuery;
+ try {
+ parsedQuery = parseQuery(query, parameters);
+ } catch (error) {
+ console.error('Failed to parse query:', error);
+ return { isLoading: false, isFetching: false, data: [], error };
+ }
+
+ const { sqlStatement, parameters: queryParameters } = parsedQuery;
+
const [data, setData] = React.useState([]);
const [error, setError] = React.useState(undefined);
const [isLoading, setIsLoading] = React.useState(true);
const [isFetching, setIsFetching] = React.useState(true);
const [tables, setTables] = React.useState([]);
- const memoizedParams = React.useMemo(() => parameters, [...parameters]);
+ const memoizedParams = React.useMemo(() => parameters, [...queryParameters]);
const memoizedOptions = React.useMemo(() => options, [JSON.stringify(options)]);
const abortController = React.useRef(new AbortController());
@@ -78,6 +88,7 @@ export const useQuery = (
const result = await powerSync.getAll(sqlStatement, parameters);
handleResult(result);
} catch (e) {
+ console.error('Failed to fetch data:', e);
handleError(e);
}
};
@@ -87,6 +98,7 @@ export const useQuery = (
const tables = await powerSync.resolveTables(sqlStatement, memoizedParams, memoizedOptions);
setTables(tables);
} catch (e) {
+ console.error('Failed to fetch tables:', e);
handleError(e);
}
};
diff --git a/packages/react/tests/useQuery.test.tsx b/packages/react/tests/useQuery.test.tsx
index 7206b468..a7ea4d3f 100644
--- a/packages/react/tests/useQuery.test.tsx
+++ b/packages/react/tests/useQuery.test.tsx
@@ -3,6 +3,7 @@ import { renderHook, waitFor } from '@testing-library/react';
import { vi, describe, expect, it, afterEach } from 'vitest';
import { useQuery } from '../src/hooks/useQuery';
import { PowerSyncContext } from '../src/hooks/PowerSyncContext';
+import * as commonSdk from '@powersync/common';
const mockPowerSync = {
currentStatus: { status: 'initial' },
@@ -25,7 +26,7 @@ describe('useQuery', () => {
it('should error when PowerSync is not set', async () => {
const { result } = renderHook(() => useQuery('SELECT * from lists'));
- const currentResult = await result.current;
+ const currentResult = result.current;
expect(currentResult.error).toEqual(Error('PowerSync not configured.'));
expect(currentResult.isLoading).toEqual(false);
expect(currentResult.data).toEqual([]);
@@ -37,7 +38,7 @@ describe('useQuery', () => {
);
const { result } = renderHook(() => useQuery('SELECT * from lists'), { wrapper });
- const currentResult = await result.current;
+ const currentResult = result.current;
expect(currentResult.isLoading).toEqual(true);
});
@@ -47,7 +48,7 @@ describe('useQuery', () => {
);
const { result } = renderHook(() => useQuery('SELECT * from lists', [], { runQueryOnce: true }), { wrapper });
- const currentResult = await result.current;
+ const currentResult = result.current;
expect(currentResult.isLoading).toEqual(true);
waitFor(
@@ -68,7 +69,7 @@ describe('useQuery', () => {
);
const { result } = renderHook(() => useQuery('SELECT * from lists', [], { runQueryOnce: true }), { wrapper });
- const currentResult = await result.current;
+ const currentResult = result.current;
expect(currentResult.isLoading).toEqual(true);
let refresh;
@@ -104,7 +105,7 @@ describe('useQuery', () => {
);
const { result } = renderHook(() => useQuery('SELECT * from lists', [], { runQueryOnce: true }), { wrapper });
- const currentResult = await result.current;
+ const currentResult = result.current;
waitFor(
async () => {
@@ -132,7 +133,7 @@ describe('useQuery', () => {
);
const { result } = renderHook(() => useQuery('SELECT * from lists', []), { wrapper });
- const currentResult = await result.current;
+ const currentResult = result.current;
waitFor(
async () => {
@@ -142,5 +143,47 @@ describe('useQuery', () => {
);
});
+ it('should accept compilable queries', async () => {
+ const wrapper = ({ children }) => (
+ {children}
+ );
+
+ const { result } = renderHook(
+ () => useQuery({ execute: () => [] as any, compile: () => ({ sql: 'SELECT * from lists', parameters: [] }) }),
+ { wrapper }
+ );
+ const currentResult = result.current;
+ expect(currentResult.isLoading).toEqual(true);
+ });
+
+ // The test returns unhandled errors when run with all the others.
+ // TODO: Fix the test so that there are no unhandled errors (this may be a vitest or @testing-library/react issue)
+ it.skip('should show an error if parsing the query results in an error', async () => {
+ const wrapper = ({ children }) => (
+ {children}
+ );
+ vi.spyOn(commonSdk, 'parseQuery').mockReturnValue(Error('error') as any);
+
+ const { result } = renderHook(
+ () =>
+ useQuery({
+ execute: () => [] as any,
+ compile: () => ({ sql: 'SELECT * from lists', parameters: ['param'] })
+ }),
+ { wrapper }
+ );
+ const currentResult = result.current;
+
+ waitFor(
+ async () => {
+ expect(currentResult.isLoading).toEqual(false);
+ expect(currentResult.isFetching).toEqual(false);
+ expect(currentResult.data).toEqual([]);
+ expect(currentResult.error).toEqual(Error('error'));
+ },
+ { timeout: 100 }
+ );
+ });
+
// TODO: Add tests for powersync.onChangeWithCallback path
});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 2e056f6f..d443c80e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1154,6 +1154,9 @@ importers:
typescript:
specifier: ^5.1.3
version: 5.4.5
+ vitest:
+ specifier: ^1.5.2
+ version: 1.5.2(@types/node@20.12.5)
packages/kysely-driver:
dependencies:
@@ -17385,6 +17388,14 @@ packages:
chai: 4.4.1
dev: true
+ /@vitest/expect@1.5.2:
+ resolution: {integrity: sha512-rf7MTD1WCoDlN3FfYJ9Llfp0PbdtOMZ3FIF0AVkDnKbp3oiMW1c8AmvRZBcqbAhDUAvF52e9zx4WQM1r3oraVA==}
+ dependencies:
+ '@vitest/spy': 1.5.2
+ '@vitest/utils': 1.5.2
+ chai: 4.4.1
+ dev: true
+
/@vitest/runner@1.3.1:
resolution: {integrity: sha512-5FzF9c3jG/z5bgCnjr8j9LNq/9OxV2uEBAITOXfoe3rdZJTdO7jzThth7FXv/6b+kdY65tpRQB7WaKhNZwX+Kg==}
dependencies:
@@ -17393,6 +17404,14 @@ packages:
pathe: 1.1.2
dev: true
+ /@vitest/runner@1.5.2:
+ resolution: {integrity: sha512-7IJ7sJhMZrqx7HIEpv3WrMYcq8ZNz9L6alo81Y6f8hV5mIE6yVZsFoivLZmr0D777klm1ReqonE9LyChdcmw6g==}
+ dependencies:
+ '@vitest/utils': 1.5.2
+ p-limit: 5.0.0
+ pathe: 1.1.2
+ dev: true
+
/@vitest/snapshot@1.3.1:
resolution: {integrity: sha512-EF++BZbt6RZmOlE3SuTPu/NfwBF6q4ABS37HHXzs2LUVPBLx2QoY/K0fKpRChSo8eLiuxcbCVfqKgx/dplCDuQ==}
dependencies:
@@ -17401,12 +17420,26 @@ packages:
pretty-format: 29.7.0
dev: true
+ /@vitest/snapshot@1.5.2:
+ resolution: {integrity: sha512-CTEp/lTYos8fuCc9+Z55Ga5NVPKUgExritjF5VY7heRFUfheoAqBneUlvXSUJHUZPjnPmyZA96yLRJDP1QATFQ==}
+ dependencies:
+ magic-string: 0.30.9
+ pathe: 1.1.2
+ pretty-format: 29.7.0
+ dev: true
+
/@vitest/spy@1.3.1:
resolution: {integrity: sha512-xAcW+S099ylC9VLU7eZfdT9myV67Nor9w9zhf0mGCYJSO+zM2839tOeROTdikOi/8Qeusffvxb/MyBSOja1Uig==}
dependencies:
tinyspy: 2.2.1
dev: true
+ /@vitest/spy@1.5.2:
+ resolution: {integrity: sha512-xCcPvI8JpCtgikT9nLpHPL1/81AYqZy1GCy4+MCHBE7xi8jgsYkULpW5hrx5PGLgOQjUpb6fd15lqcriJ40tfQ==}
+ dependencies:
+ tinyspy: 2.2.1
+ dev: true
+
/@vitest/utils@1.3.1:
resolution: {integrity: sha512-d3Waie/299qqRyHTm2DjADeTaNdNSVsnwHPWrs20JMpjh6eiVq7ggggweO8rc4arhf6rRkWuHKwvxGvejUXZZQ==}
dependencies:
@@ -17416,6 +17449,15 @@ packages:
pretty-format: 29.7.0
dev: true
+ /@vitest/utils@1.5.2:
+ resolution: {integrity: sha512-sWOmyofuXLJ85VvXNsroZur7mOJGiQeM0JN3/0D1uU8U9bGFM69X1iqHaRXl6R8BwaLY6yPCogP257zxTzkUdA==}
+ dependencies:
+ diff-sequences: 29.6.3
+ estree-walker: 3.0.3
+ loupe: 2.3.7
+ pretty-format: 29.7.0
+ dev: true
+
/@volar/language-core@2.2.0-alpha.7:
resolution: {integrity: sha512-igpp+nTkyl8faVzRJMpSCeA4XlBJ5UVSyc/WGyksmUmP10YbfufbcQCFlxEXv2uMBV+a3L4JVCj+Vju+08FOSA==}
dependencies:
@@ -34160,6 +34202,11 @@ packages:
engines: {node: '>=14.0.0'}
dev: true
+ /tinypool@0.8.4:
+ resolution: {integrity: sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==}
+ engines: {node: '>=14.0.0'}
+ dev: true
+
/tinyspy@2.2.1:
resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==}
engines: {node: '>=14.0.0'}
@@ -35191,6 +35238,27 @@ packages:
- terser
dev: true
+ /vite-node@1.5.2(@types/node@20.12.5):
+ resolution: {integrity: sha512-Y8p91kz9zU+bWtF7HGt6DVw2JbhyuB2RlZix3FPYAYmUyZ3n7iTp8eSyLyY6sxtPegvxQtmlTMhfPhUfCUF93A==}
+ engines: {node: ^18.0.0 || >=20.0.0}
+ hasBin: true
+ dependencies:
+ cac: 6.7.14
+ debug: 4.3.4(supports-color@8.1.1)
+ pathe: 1.1.2
+ picocolors: 1.0.0
+ vite: 5.2.8(@types/node@20.12.5)
+ transitivePeerDependencies:
+ - '@types/node'
+ - less
+ - lightningcss
+ - sass
+ - stylus
+ - sugarss
+ - supports-color
+ - terser
+ dev: true
+
/vite-plugin-pwa@0.19.2(vite@5.1.5)(workbox-build@7.0.0)(workbox-window@7.0.0):
resolution: {integrity: sha512-LSQJFPxCAQYbRuSyc9EbRLRqLpaBA9onIZuQFomfUYjWSgHuQLonahetDlPSC9zsxmkSEhQH8dXZN8yL978h3w==}
engines: {node: '>=16.0.0'}
@@ -35616,6 +35684,42 @@ packages:
fsevents: 2.3.3
dev: true
+ /vite@5.2.8(@types/node@20.12.5):
+ resolution: {integrity: sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA==}
+ engines: {node: ^18.0.0 || >=20.0.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^18.0.0 || >=20.0.0
+ less: '*'
+ lightningcss: ^1.21.0
+ sass: '*'
+ stylus: '*'
+ sugarss: '*'
+ terser: ^5.4.0
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ less:
+ optional: true
+ lightningcss:
+ optional: true
+ sass:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ dependencies:
+ '@types/node': 20.12.5
+ esbuild: 0.20.2
+ postcss: 8.4.38
+ rollup: 4.13.0
+ optionalDependencies:
+ fsevents: 2.3.3
+ dev: true
+
/vite@5.2.8(sass@1.71.1):
resolution: {integrity: sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA==}
engines: {node: ^18.0.0 || >=20.0.0}
@@ -35708,6 +35812,62 @@ packages:
- terser
dev: true
+ /vitest@1.5.2(@types/node@20.12.5):
+ resolution: {integrity: sha512-l9gwIkq16ug3xY7BxHwcBQovLZG75zZL0PlsiYQbf76Rz6QGs54416UWMtC0jXeihvHvcHrf2ROEjkQRVpoZYw==}
+ engines: {node: ^18.0.0 || >=20.0.0}
+ hasBin: true
+ peerDependencies:
+ '@edge-runtime/vm': '*'
+ '@types/node': ^18.0.0 || >=20.0.0
+ '@vitest/browser': 1.5.2
+ '@vitest/ui': 1.5.2
+ happy-dom: '*'
+ jsdom: '*'
+ peerDependenciesMeta:
+ '@edge-runtime/vm':
+ optional: true
+ '@types/node':
+ optional: true
+ '@vitest/browser':
+ optional: true
+ '@vitest/ui':
+ optional: true
+ happy-dom:
+ optional: true
+ jsdom:
+ optional: true
+ dependencies:
+ '@types/node': 20.12.5
+ '@vitest/expect': 1.5.2
+ '@vitest/runner': 1.5.2
+ '@vitest/snapshot': 1.5.2
+ '@vitest/spy': 1.5.2
+ '@vitest/utils': 1.5.2
+ acorn-walk: 8.3.2
+ chai: 4.4.1
+ debug: 4.3.4(supports-color@8.1.1)
+ execa: 8.0.1
+ local-pkg: 0.5.0
+ magic-string: 0.30.9
+ pathe: 1.1.2
+ picocolors: 1.0.0
+ std-env: 3.7.0
+ strip-literal: 2.0.0
+ tinybench: 2.6.0
+ tinypool: 0.8.4
+ vite: 5.2.8(@types/node@20.12.5)
+ vite-node: 1.5.2(@types/node@20.12.5)
+ why-is-node-running: 2.2.2
+ transitivePeerDependencies:
+ - less
+ - lightningcss
+ - sass
+ - stylus
+ - sugarss
+ - supports-color
+ - terser
+ dev: true
+
/vlq@1.0.1:
resolution: {integrity: sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==}