mirror of
https://github.com/penxio/penx.git
synced 2026-04-19 03:03:06 -04:00
feat: add change schema
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
139
apps/electron/src/main/routers/change.ts
Normal file
139
apps/electron/src/main/routers/change.ts
Normal 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
|
||||
@@ -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',
|
||||
|
||||
@@ -98,7 +98,7 @@ export const AppProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||
)
|
||||
}
|
||||
|
||||
console.log('appProvider========session:', session)
|
||||
// console.log('appProvider========session:', session)
|
||||
|
||||
if (isLoading) return null
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -148,6 +148,8 @@ export function JournalQuickInput({
|
||||
title: noteTitle,
|
||||
})
|
||||
}, 0)
|
||||
|
||||
appEmitter.emit('REFRESH_COMMANDS')
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -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 },
|
||||
})
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export { nodes } from './schema/nodes'
|
||||
export { embeddings } from './schema/embeddings'
|
||||
export { changes } from './schema/change'
|
||||
|
||||
31
packages/db/src/schema/change.ts
Normal file
31
packages/db/src/schema/change.ts
Normal 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
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ export function PageQuickInput() {
|
||||
},
|
||||
onConfirm: () => {
|
||||
navigation.pop()
|
||||
alert('hlllo')
|
||||
appEmitter.emit('SUBMIT_QUICK_INPUT')
|
||||
},
|
||||
}}
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
72
packages/worker/src/lib/change.helper.ts
Normal file
72
packages/worker/src/lib/change.helper.ts
Normal 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)
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user