Skip to content

Commit 5bff870

Browse files
committed
updates
1 parent 718beb3 commit 5bff870

15 files changed

Lines changed: 134 additions & 134 deletions

File tree

apps/app/src/entities/chat/sync.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@ import { persistedCollectionOptions } from '@tanstack/browser-db-sqlite-persiste
33
import { electricCollectionOptions } from '@tanstack/electric-db-collection'
44
import { BasicIndex, createCollection } from '@tanstack/react-db'
55
import { type } from 'arktype'
6-
import { shapeOptions } from '~/lib/electric'
76
import { orpc } from '~/lib/orpc'
8-
import { persistence } from '~/lib/sync'
7+
import { persistence, shapeOptions } from '~/lib/sync'
98

109
export const chatsSchema = type({
1110
id: 'string',

apps/app/src/entities/connection/query.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { Connection, ConnectionResource } from '~/entities/connection/sync'
44
import { isConnectionError } from '@conar/shared/utils/connections'
55
import { SafeURL } from '@conar/shared/utils/safe-url'
66
import { Result } from 'better-result'
7+
import { memoize } from 'memoza'
78
import { createStore } from 'seitu'
89
import { toast } from 'sonner'
910
import { connectionStringStorage } from '~/lib/connection-string-storage'
@@ -20,7 +21,7 @@ export async function connectionToQueryParams(connection: Connection): Promise<Q
2021
}
2122
}
2223

23-
export async function connectionResourceToQueryParams(connectionResource: ConnectionResource): Promise<QueryParams> {
24+
export const connectionResourceToQueryParams = memoize(async (connectionResource: ConnectionResource): Promise<QueryParams> => {
2425
const connection = connectionsCollection.get(connectionResource.connectionId)!
2526
const connectionString = new SafeURL(await connectionStringStorage.decrypt(connection.id))
2627
connectionString.pathname = connectionResource.name || ''
@@ -31,7 +32,7 @@ export async function connectionResourceToQueryParams(connectionResource: Connec
3132
resourceId: connectionResource.id,
3233
log: ({ promise, query, values }) => logQuery({ resourceId: connectionResource.id, promise, query, values }),
3334
}
34-
}
35+
})
3536

3637
export interface QueryParams {
3738
connectionString: string

apps/app/src/entities/connection/sync.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,8 @@ import { electricCollectionOptions } from '@tanstack/electric-db-collection'
66
import { BasicIndex, createCollection, createOptimisticAction } from '@tanstack/react-db'
77
import { type } from 'arktype'
88
import { connectionStringStorage } from '~/lib/connection-string-storage'
9-
import { shapeOptions } from '~/lib/electric'
109
import { orpc } from '~/lib/orpc'
11-
import { persistence } from '~/lib/sync'
10+
import { persistence, shapeOptions } from '~/lib/sync'
1211

1312
export const connectionsSchema = type({
1413
id: 'string',

apps/app/src/entities/connection/utils/fetching.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ import { connectionsCollection } from '../sync'
1616

1717
export async function prefetchConnectionResourceCore(connectionResource: ConnectionResource) {
1818
const connection = connectionsCollection.get(connectionResource.connectionId)!
19-
const info = connectionStringStorage.get(connection.id)
19+
const connectionString = connectionStringStorage.get(connection.id)
2020

21-
if (connection.passwordExists && !info?.metadata?.isPasswordPopulated) {
21+
if (connection.passwordExists && !connectionString?.metadata?.isPasswordPopulated) {
2222
return
2323
}
2424

apps/app/src/entities/query/sync.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@ import { persistedCollectionOptions } from '@tanstack/browser-db-sqlite-persiste
22
import { electricCollectionOptions } from '@tanstack/electric-db-collection'
33
import { BasicIndex, createCollection } from '@tanstack/react-db'
44
import { type } from 'arktype'
5-
import { shapeOptions } from '~/lib/electric'
65
import { orpc } from '~/lib/orpc'
7-
import { persistence } from '~/lib/sync'
6+
import { persistence, shapeOptions } from '~/lib/sync'
87

98
export const queriesSchema = type({
109
id: 'string',

apps/app/src/lib/auth.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import { bearer } from 'better-auth/plugins'
44
import { createAuthClient } from 'better-auth/react'
55
import { createWebStorageValue } from 'seitu/web'
66
import { toast } from 'sonner'
7+
import { router } from '~/main'
78
import { apiUrl } from '../utils/utils'
8-
import { clearDb } from './sync'
9+
import { connectionStringStorage } from './connection-string-storage'
910

1011
const BEARER_TOKEN_KEY = 'tamery.bearer_token'
1112

@@ -61,6 +62,6 @@ export async function isSignedIn() {
6162
export async function fullSignOut() {
6263
await authClient.signOut()
6364
bearerToken.clear()
64-
await clearDb()
65-
window.location.reload()
65+
router.navigate({ to: '/auth' })
66+
await connectionStringStorage.clear()
6667
}

apps/app/src/lib/connection-string-storage.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ import { memoize } from 'memoza'
77
import { useSubscription } from 'seitu/react'
88
import { createIndexedDbStorage } from 'seitu/web'
99

10-
const safeStorageAvailable = memoize(() => window.electron ? window.electron.safeStorage.isEncryptionAvailable() : false)
10+
const safeStorageAvailable = memoize(async () => window.electron ? window.electron.safeStorage.isEncryptionAvailable() : false)
1111

1212
function importAesKey(raw: Uint8Array<ArrayBuffer>) {
1313
return crypto.subtle.importKey('raw', raw, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt'])
1414
}
1515

16-
const connectionsStringsStorage = createIndexedDbStorage({
16+
const storage = createIndexedDbStorage({
1717
databaseName: 'secure-storage',
1818
storeName: 'connections-strings',
1919
schemas: {
@@ -38,13 +38,13 @@ const connectionsStringsStorage = createIndexedDbStorage({
3838
})
3939

4040
function resetEncryptionKey() {
41-
return connectionsStringsStorage.set({ encryptionKey: null, connectionStrings: {} })
41+
return storage.set({ encryptionKey: null, connectionStrings: {} })
4242
}
4343

4444
const getEncryptionKey = memoize(async (): Promise<CryptoKey> => {
45-
await connectionsStringsStorage.ready
45+
await storage.ready
4646

47-
const stored = connectionsStringsStorage.get().encryptionKey
47+
const stored = storage.get().encryptionKey
4848

4949
if (stored instanceof CryptoKey)
5050
return stored
@@ -62,7 +62,7 @@ const getEncryptionKey = memoize(async (): Promise<CryptoKey> => {
6262
}
6363

6464
const raw = crypto.getRandomValues(new Uint8Array(32))
65-
await connectionsStringsStorage.set({ encryptionKey: await safeStorage.encryptString(bytesToBase64(raw)) })
65+
await storage.set({ encryptionKey: await safeStorage.encryptString(bytesToBase64(raw)) })
6666
return importAesKey(raw)
6767
}
6868

@@ -72,7 +72,7 @@ const getEncryptionKey = memoize(async (): Promise<CryptoKey> => {
7272
await resetEncryptionKey()
7373

7474
const key = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt'])
75-
await connectionsStringsStorage.set({ encryptionKey: key })
75+
await storage.set({ encryptionKey: key })
7676
return key
7777
})
7878

@@ -85,19 +85,19 @@ async function decryptConnectionString(encryptedConnectionString: string) {
8585
}
8686

8787
export const connectionStringStorage = {
88-
ready: connectionsStringsStorage.ready,
89-
has: (id: string) => id in connectionsStringsStorage.get().connectionStrings,
90-
get: (id: string) => connectionsStringsStorage.get().connectionStrings[id],
88+
ready: storage.ready,
89+
has: (id: string) => id in storage.get().connectionStrings,
90+
get: (id: string) => storage.get().connectionStrings[id],
9191
decrypt: (id: string): Promise<string> => {
92-
const record = connectionsStringsStorage.get().connectionStrings[id]
92+
const record = connectionStringStorage.get(id)
9393
if (!record)
9494
throw new Error(`No connection string found for connection "${id}"`)
9595
return decryptConnectionString(record.encrypted)
9696
},
9797
set: async (id: string, connectionString: string) => {
9898
const url = new SafeURL(connectionString)
9999
const encrypted = await encryptConnectionString(connectionString)
100-
await connectionsStringsStorage.set(prev => ({
100+
await storage.set(prev => ({
101101
connectionStrings: {
102102
...prev.connectionStrings,
103103
[id]: {
@@ -113,15 +113,15 @@ export const connectionStringStorage = {
113113
}))
114114
},
115115
remove: async (id: string) => {
116-
await connectionsStringsStorage.set((prev) => {
116+
await storage.set((prev) => {
117117
const connectionStrings = { ...prev.connectionStrings }
118118
delete connectionStrings[id]
119119
return { connectionStrings }
120120
})
121121
},
122-
clear: () => connectionsStringsStorage.set({ connectionStrings: {} }),
122+
clear: () => storage.set({ connectionStrings: {} }),
123123
}
124124

125125
export function useConnectionString(id: string) {
126-
return useSubscription(connectionsStringsStorage, { selector: state => state.connectionStrings[id] })
126+
return useSubscription(storage, { selector: state => state.connectionStrings[id] })
127127
}

apps/app/src/lib/db.ts

Whitespace-only changes.

apps/app/src/lib/electric.ts

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +0,0 @@
1-
import type { ElectricCollectionConfig } from '@tanstack/electric-db-collection'
2-
import { snakeCamelMapper } from '@electric-sql/client'
3-
import { apiUrl } from '~/utils/utils'
4-
import { bearerToken } from './auth'
5-
6-
export function shapeOptions(entity: string) {
7-
return {
8-
url: new URL(`/shapes/${entity}`, apiUrl).toString(),
9-
columnMapper: snakeCamelMapper(),
10-
liveSse: true,
11-
headers: {
12-
Authorization: () => {
13-
const token = bearerToken.get()
14-
return token ? `Bearer ${token}` : ''
15-
},
16-
},
17-
fetchClient: ((url, init) => globalThis.fetch(url, {
18-
...init,
19-
credentials: 'include',
20-
})) as typeof fetch,
21-
} satisfies ElectricCollectionConfig['shapeOptions']
22-
}

apps/app/src/lib/sync.ts

Lines changed: 66 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
import type { ElectricCollectionConfig } from '@tanstack/electric-db-collection'
12
import { GITHUB_REPO_NAME } from '@conar/shared/constants'
3+
import { FetchError, snakeCamelMapper } from '@electric-sql/client'
24
import { BrowserCollectionCoordinator, createBrowserWASQLitePersistence, openBrowserWASQLiteOPFSDatabase } from '@tanstack/browser-db-sqlite-persistence'
3-
import { connectionStringStorage } from '~/lib/connection-string-storage'
4-
import { posthog } from '~/lib/posthog'
5+
import { apiUrl } from '~/utils/utils'
6+
import { bearerToken } from './auth'
7+
import { connectionStringStorage } from './connection-string-storage'
8+
import { posthog } from './posthog'
59

610
const DATABASE_NAME = `${GITHUB_REPO_NAME}.sqlite`
711

@@ -20,17 +24,29 @@ export const persistence = createBrowserWASQLitePersistence({
2024
schemaMismatchPolicy: 'reset',
2125
})
2226

23-
if (import.meta.env.DEV) {
24-
async function showCollectionData(name: string) {
25-
const collections = await database.execute('SELECT * FROM collection_registry') as { collection_id: string, table_name: string, schema_version: number }[]
26-
const collection = await database.execute(`SELECT * FROM ${collections.find(c => c.collection_id === name)?.table_name}`) as { key: string, metadata: unknown, row_version: number, value: string }[]
27-
28-
// eslint-disable-next-line no-console
29-
console.log(collection.map(c => JSON.parse(c.value)))
30-
}
27+
export function shapeOptions(entity: string) {
28+
return {
29+
url: new URL(`/shapes/${entity}`, apiUrl).toString(),
30+
columnMapper: snakeCamelMapper(),
31+
liveSse: true,
32+
headers: {
33+
Authorization: () => {
34+
const token = bearerToken.get()
35+
return token ? `Bearer ${token}` : ''
36+
},
37+
},
38+
fetchClient: ((url, init) => globalThis.fetch(url, {
39+
...init,
40+
credentials: 'include',
41+
})) as typeof fetch,
42+
onError(error) {
43+
if (error instanceof FetchError && error.status === 401) {
44+
return
45+
}
3146

32-
// @ts-expect-error window is not typed
33-
window.showCollection = showCollectionData
47+
throw error
48+
},
49+
} satisfies ElectricCollectionConfig['shapeOptions']
3450
}
3551

3652
export async function clearDb() {
@@ -52,3 +68,41 @@ export async function clearDb() {
5268
}
5369
await connectionStringStorage.clear()
5470
}
71+
72+
async function getCollectionTableName(name: string) {
73+
const collections = await database.execute('SELECT * FROM collection_registry') as { collection_id: string, table_name: string, schema_version: number }[]
74+
const matching = collections.filter(c => c.collection_id === name)
75+
if (matching.length === 0)
76+
return undefined
77+
78+
return matching.toSorted((a, b) => b.schema_version - a.schema_version)[0]!.table_name
79+
}
80+
81+
export async function truncateCollectionData(name: string) {
82+
const tableName = await getCollectionTableName(name)
83+
if (!tableName) {
84+
return
85+
}
86+
87+
await database.execute(`DELETE FROM ${tableName}`)
88+
89+
// eslint-disable-next-line no-console
90+
console.log(`Truncated collection "${name}" (${tableName})`)
91+
}
92+
93+
if (import.meta.env.DEV) {
94+
async function showCollectionData(name: string) {
95+
const tableName = await getCollectionTableName(name)
96+
if (!tableName) {
97+
return
98+
}
99+
100+
const collection = await database.execute(`SELECT * FROM ${tableName}`) as { key: string, metadata: unknown, row_version: number, value: string }[]
101+
102+
// eslint-disable-next-line no-console
103+
console.log(collection.map(c => JSON.parse(c.value)))
104+
}
105+
106+
// @ts-expect-error window is not typed
107+
window.showCollection = showCollectionData
108+
}

0 commit comments

Comments
 (0)