feat: add change schema

This commit is contained in:
0xzio
2025-09-12 08:32:29 +08:00
parent 1c2e8cf5c1
commit 55fa8df560
15 changed files with 346 additions and 73 deletions

View File

@@ -2,6 +2,7 @@ import { sql } from 'drizzle-orm';
import { db } from '@penx/db/client';
import { embeddings } from '@penx/db/schema/embeddings';
import { nodes } from '@penx/db/schema/nodes';
import { changes } from '@penx/db/schema/change';
export async function initPGLite() {
@@ -18,6 +19,10 @@ export async function initPGLite() {
await createEmbeddingTable()
}
if (!tableStatus.changeExists) {
await createChangeTable()
}
console.log('PGLite database initialized successfully')
} catch (error: any) {
console.error('Failed to initialize PGLite database:', error)
@@ -28,6 +33,7 @@ export async function initPGLite() {
async function checkTablesExist() {
let nodeExists = false
let embeddingExists = false
let changeExists = false
try {
await db.select().from(nodes).limit(1)
@@ -45,7 +51,15 @@ async function checkTablesExist() {
console.log('Embeddings table does not exist, will create it')
}
return { nodeExists, embeddingExists }
try {
await db.select().from(changes).limit(1)
changeExists = true
console.log('Change table exists')
} catch (error: any) {
console.log('Change table does not exist, will create it')
}
return { nodeExists, embeddingExists, changeExists }
}
async function createNodeTable() {
@@ -150,4 +164,46 @@ async function createEmbeddingTable() {
throw error
}
}
}
async function createChangeTable() {
try {
// Create change table
await db.execute(sql`
CREATE TABLE "change" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "change_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
"operation" text NOT NULL,
"space_id" uuid NOT NULL,
"key" text NOT NULL,
"data" jsonb,
"synced" integer DEFAULT 0 NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL
)
`)
console.log('Created change table')
// Create indexes
await db.execute(
sql`CREATE INDEX "idx_change_space_id" ON "change" USING btree ("space_id")`,
)
await db.execute(
sql`CREATE INDEX "idx_change_synced" ON "change" USING btree ("synced")`,
)
await db.execute(
sql`CREATE INDEX "idx_change_space_synced" ON "change" USING btree ("space_id","synced")`,
)
console.log('Created change table indexes')
} catch (error: any) {
// If table already exists, that's fine
if (
error.message?.includes('already exists') ||
error.message?.includes('duplicate key')
) {
console.log('Change table or index already exists, skipping creation')
} else {
console.error('Change table creation error:', error)
throw error
}
}
}

View File

@@ -0,0 +1,139 @@
import { zValidator } from '@hono/zod-validator'
import { and, asc, eq, inArray } from 'drizzle-orm'
import { Hono } from 'hono'
import { produce } from 'immer'
import { z } from 'zod'
import { db } from '@penx/db/client'
import { changes } from '@penx/db/schema'
import { Change } from '@penx/db/schema/change'
import { auth } from '../lib/auth'
const app = new Hono()
app.get('/listAll', async (c) => {
return c.json({
success: true,
data: await db.query.changes.findMany(),
})
})
app.get(
'/listBySpace',
zValidator(
'query',
z.object({
spaceId: z.string(),
}),
),
async (c) => {
const input = c.req.valid('query')
const list = await db.query.changes.findMany({
where: and(eq(changes.spaceId, input.spaceId), eq(changes.synced, 0)),
orderBy: asc(changes.id),
})
return c.json({
success: true,
data: list,
})
},
)
app.post(
'/create',
// auth,
zValidator(
'json',
z.object({
operation: z.any(),
spaceId: z.string(),
synced: z.number(),
createdAt: z.any(),
key: z.string(),
data: z.any(),
}),
),
async (c) => {
const input = c.req.valid('json')
await db.insert(changes).values({
...input,
createdAt: new Date(input.createdAt),
} as Change)
return c.json({
success: true,
})
},
)
app.post(
'/update',
// auth,
zValidator(
'json',
z.object({
id: z.any(),
operation: z.any(),
spaceId: z.string(),
synced: z.number(),
createdAt: z.any(),
key: z.string(),
data: z.any(),
}),
),
async (c) => {
const { id, ...rest } = c.req.valid('json')
const data = produce(rest, (draft) => {
draft.createdAt = new Date(draft.createdAt)
if (draft?.data?.updatedAt) {
draft.data.updatedAt = new Date(draft.data.updatedAt)
}
if (draft?.data?.createdAt) {
draft.data.createdAt = new Date(draft.data.createdAt)
}
})
await db.update(changes).set(data).where(eq(changes.id, id))
return c.json({
success: true,
})
},
)
app.post(
'/deleteOne',
// auth,
zValidator(
'json',
z.object({
id: z.any(),
}),
),
async (c) => {
const { id } = c.req.valid('json')
await db.delete(changes).where(eq(changes.id, id))
return c.json({
success: true,
})
},
)
app.post(
'/deleteByIds',
// auth,
zValidator(
'json',
z.object({
ids: z.array(z.any()),
}),
),
async (c) => {
const { ids } = c.req.valid('json')
await db.delete(changes).where(inArray(changes.id, ids))
return c.json({
success: true,
})
},
)
export default app

View File

@@ -27,6 +27,7 @@ import { getNodeEmbedding } from './lib/getNodeEmbedding'
import { retrieveCreations } from './lib/retrieveCreations'
import aiRouter from './routers/ai'
import bookmarkRouter from './routers/bookmark'
import changeRouter from './routers/change'
import dbProxyRouter from './routers/db-proxy'
import extensionRouter from './routers/extension'
import nodeRouter from './routers/node'
@@ -262,30 +263,7 @@ export class HonoServer {
api.route('/node', nodeRouter)
api.route('/ai', aiRouter)
api.route('/extension', extensionRouter)
api.post(
'/createChange',
// auth,
zValidator(
'json',
z.object({
operation: z.any(),
spaceId: z.string(),
synced: z.number(),
createdAt: z.any(),
key: z.string(),
data: z.any(),
}),
),
async (c) => {
const input = c.req.valid('json')
this.windows.panelWindow?.webContents.send('create-change', input)
return c.json({
success: true,
})
},
)
api.route('/change', changeRouter)
api.post(
'/rag/retrieve',

View File

@@ -98,7 +98,7 @@ export const AppProvider: FC<PropsWithChildren> = ({ children }) => {
)
}
console.log('appProvider========session:', session)
// console.log('appProvider========session:', session)
if (isLoading) return null

View File

@@ -115,7 +115,7 @@ export class AppService {
const spaces = await localDB.listAllSpaceByUserId(session.userId)
const space = spaces.find((s) => s.props.isRemote)
console.log('=======spaces:', spaces)
// console.log('=======spaces:', spaces)
if (space) {
await syncNodesToLocal(space.id)

View File

@@ -148,6 +148,8 @@ export function JournalQuickInput({
title: noteTitle,
})
}, 0)
appEmitter.emit('REFRESH_COMMANDS')
}
useEffect(() => {

View File

@@ -3,7 +3,7 @@ import { PGlite } from '@electric-sql/pglite'
import { vector } from '@electric-sql/pglite/vector'
import { drizzle } from 'drizzle-orm/pglite'
import { app } from 'electron'
import { embeddings, nodes } from './schema'
import { changes, embeddings, nodes } from './schema'
const dbPath = join(app.getPath('userData'), 'penx-db')
@@ -11,4 +11,7 @@ export const pg = new PGlite(dbPath, {
extensions: { vector },
})
export const db = drizzle({ client: pg, schema: { nodes, embeddings } })
export const db = drizzle({
client: pg,
schema: { nodes, embeddings, changes },
})

View File

@@ -1,2 +1,3 @@
export { nodes } from './schema/nodes'
export { embeddings } from './schema/embeddings'
export { changes } from './schema/change'

View File

@@ -0,0 +1,31 @@
import { sql } from 'drizzle-orm'
import {
index,
integer,
jsonb,
pgEnum,
pgTable,
text,
timestamp,
uuid,
} from 'drizzle-orm/pg-core'
export const changes = pgTable(
'change',
{
id: integer('id').notNull().primaryKey().generatedAlwaysAsIdentity(),
operation: text('operation').notNull(),
spaceId: uuid('space_id').notNull(),
key: text('key').notNull(),
data: jsonb('data'),
synced: integer('synced').notNull().default(0), // 0 - not synced, 1 - synced
createdAt: timestamp('created_at', { mode: 'date' }).notNull().defaultNow(),
},
(table) => [
index('idx_change_space_id').on(table.spaceId),
index('idx_change_synced').on(table.synced),
index('idx_change_space_synced').on(table.spaceId, table.synced),
],
)
export type Change = typeof changes.$inferSelect

View File

@@ -1,6 +1,11 @@
import { get } from 'idb-keyval'
import ky from 'ky'
import { ACTIVE_SPACE, APP_LOCAL_HOST, isExtension } from '@penx/constants'
import {
ACTIVE_SPACE,
APP_LOCAL_HOST,
isDesktop,
isExtension,
} from '@penx/constants'
import { idb } from '@penx/indexeddb'
import {
IAreaNode,
@@ -382,13 +387,12 @@ class LocalDB {
data,
} as IChange
if (isExtension) {
if (isExtension || isDesktop) {
await ky
.post(`${APP_LOCAL_HOST}/api/createChange`, {
.post(`${APP_LOCAL_HOST}/api/change/create`, {
json: change,
})
.json()
//
} else {
await idb.change.add(change)
}

View File

@@ -26,6 +26,7 @@ export function PageQuickInput() {
},
onConfirm: () => {
navigation.pop()
alert('hlllo')
appEmitter.emit('SUBMIT_QUICK_INPUT')
},
}}

View File

@@ -18,6 +18,7 @@ export const SyncIntervalSelect = ({}: Props) => {
queryKey: ['syncInterval'],
queryFn: async () => {
const interval = await get(key)
return (interval as number) || 1000 * 60 * 30
},
})
@@ -61,20 +62,21 @@ export const SyncIntervalSelect = ({}: Props) => {
<span>
<Trans>Time interval</Trans>
</span>
<Select value={value} onValueChange={setValue}>
<Select
value={value}
onValueChange={async (v) => {
console.log('v========vv:', v)
setValue(v)
await set(key, Number(v))
refetch()
}}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Interval" />
</SelectTrigger>
<SelectContent>
{options.map((item) => (
<SelectItem
key={item.value}
value={item.value.toString()}
onClick={async () => {
await set(key, item.value)
refetch()
}}
>
<SelectItem key={item.value} value={item.value.toString()}>
{item.label}
</SelectItem>
))}

View File

@@ -0,0 +1,72 @@
import ky from 'ky'
import _ from 'lodash'
import { ApiRes } from '@penx/api'
import { APP_LOCAL_HOST, isDesktop, isExtension } from '@penx/constants'
import { idb } from '@penx/indexeddb'
import { IChange } from '@penx/model-type'
import { SessionData } from '@penx/types'
export const getChanges = async (session: SessionData) => {
let changes: IChange[] = []
if (isExtension || isDesktop) {
const res = await ky
.get(`${APP_LOCAL_HOST}/api/change/listBySpace`, {
searchParams: { spaceId: session.spaceId },
})
.json<ApiRes<IChange[]>>()
changes = res.data
} else {
changes = await idb.change
.where({ spaceId: session.spaceId, synced: 0 })
.sortBy('id')
}
return changes.filter((change) => {
if (
Reflect.has(change.data, 'userId') &&
change.data.userId !== session.userId
) {
return false
}
if (change.synced) return false
return true
})
}
export async function deleteChangeByIds(ids: number[]) {
if (isExtension || isDesktop) {
return ky
.post(`${APP_LOCAL_HOST}/api/change/deleteByIds`, {
json: { ids },
})
.json()
}
return idb.change.where('id').anyOf(ids).delete()
}
export async function updateChange(data: IChange) {
if (isExtension || isDesktop) {
return ky
.post(`${APP_LOCAL_HOST}/api/change/update`, {
json: data,
})
.json()
}
const { id, ...rest } = data
return idb.change.update(id, rest)
}
export async function deleteChange(id: number) {
if (isExtension || isDesktop) {
return ky
.post(`${APP_LOCAL_HOST}/api/change/deleteOne`, {
json: { id },
})
.json()
}
return idb.change.delete(id)
}

View File

@@ -1,20 +0,0 @@
import _ from 'lodash'
import { idb } from '@penx/indexeddb'
import { SessionData } from '@penx/types'
export const getChanges = async (session: SessionData) => {
const changes = await idb.change
.where({ spaceId: session.spaceId, synced: 0 })
.sortBy('id')
return changes.filter((change) => {
if (
Reflect.has(change.data, 'userId') &&
change.data.userId !== session.userId
) {
return false
}
if (change.synced) return false
return true
})
}

View File

@@ -1,6 +1,5 @@
import { get, set } from 'idb-keyval'
import { produce } from 'immer'
import ky from 'ky'
import _ from 'lodash'
import { api } from '@penx/api'
import {
@@ -9,13 +8,16 @@ import {
isMobileApp,
ROOT_HOST,
} from '@penx/constants'
import { idb } from '@penx/indexeddb'
import { checkMnemonic } from '@penx/libs/checkMnemonic'
import { localDB } from '@penx/local-db'
import { encryptByPublicKey } from '@penx/mnemonic'
import { IChange, ICreationNode, OperationType } from '@penx/model-type'
import { SessionData } from '@penx/types'
import { getChanges } from './getChanges'
import {
deleteChange,
deleteChangeByIds,
getChanges,
updateChange,
} from './change.helper'
import { mergeChanges } from './mergeChanges'
export async function syncNodesToServer() {
@@ -28,8 +30,10 @@ export async function syncNodesToServer() {
// if (!site) return
await checkMnemonic(session)
console.log('>>>>>>>>>meno:', session)
const changes = await getChanges(session)
console.log('=======>>>>>>changes:', changes)
const mergedChanges = mergeChanges(changes)
@@ -39,10 +43,10 @@ export async function syncNodesToServer() {
.filter((c) => !mergedChangeIds.includes(c.id))
.map((c) => c.id)
await idb.change.where('id').anyOf(deleteChangeIds).delete()
await deleteChangeByIds(deleteChangeIds)
for (const { id, ...rest } of mergedChanges) {
await idb.change.update(id, rest)
for (const change of mergedChanges) {
await updateChange(change)
}
const newChanges = await getChanges(session)
@@ -130,15 +134,15 @@ export async function syncNodesToServer() {
// console.log('>>>>>change synced:', change)
await api.sync(url, encryptedInput)
await idb.change.delete(change.id)
await deleteChange(change.id)
} catch (error) {
console.log('error syncing change:', error)
console.log('change>>>>>>>>>>:', change)
if (error.errorCode === 'NODE_NOT_EXISTED') {
// await idb.change.delete(change.id)
if (isDesktop) {
await idb.change.delete(change.id)
await deleteChange(change.id)
// const node = await ky
// .get(`${APP_LOCAL_HOST}/api/get`, {
// searchParams: { id: change.key },
@@ -148,7 +152,7 @@ export async function syncNodesToServer() {
// console.log('=======>>>>>NODE_NOT_EXISTED:node:', node)
}
// await idb.change.delete(change.id)
await deleteChange(change.id)
}
errors.push(error)
}