feat(0.2.1): M3 前端 PowerSync 基础 (schema/connector/db) + 依赖; fix storage sslmode
This commit is contained in:
@@ -13,6 +13,7 @@ replication:
|
|||||||
storage:
|
storage:
|
||||||
type: postgresql
|
type: postgresql
|
||||||
uri: postgresql://powersync_storage:!env PS_STORAGE_PASSWORD@pg:5432/iboard021
|
uri: postgresql://powersync_storage:!env PS_STORAGE_PASSWORD@pg:5432/iboard021
|
||||||
|
sslmode: disable
|
||||||
|
|
||||||
port: 8080
|
port: 8080
|
||||||
|
|
||||||
|
|||||||
Generated
+67
@@ -8,6 +8,8 @@
|
|||||||
"name": "iboard",
|
"name": "iboard",
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@powersync/react": "^1.10.0",
|
||||||
|
"@powersync/web": "^1.39.1",
|
||||||
"@prisma/adapter-pg": "^7.9.1",
|
"@prisma/adapter-pg": "^7.9.1",
|
||||||
"@prisma/client": "^7.7.0",
|
"@prisma/client": "^7.7.0",
|
||||||
"@zythum02/mijia-api": "^0.1.4",
|
"@zythum02/mijia-api": "^0.1.4",
|
||||||
@@ -1187,6 +1189,13 @@
|
|||||||
"url": "https://opencollective.com/libvips"
|
"url": "https://opencollective.com/libvips"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@journeyapps/wa-sqlite": {
|
||||||
|
"version": "1.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@journeyapps/wa-sqlite/-/wa-sqlite-1.7.2.tgz",
|
||||||
|
"integrity": "sha512-EVRMzqHtHgJBFcTGOgQ5/YlxrYFSXLFFztC5yo6+jQx2mqFR+a1kkLV4IirquiBzIonjmYXfBtvZ26fmI8r9Nw==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/@jridgewell/gen-mapping": {
|
"node_modules/@jridgewell/gen-mapping": {
|
||||||
"version": "0.3.13",
|
"version": "0.3.13",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||||
@@ -1615,6 +1624,52 @@
|
|||||||
"node": "^22.20 || ^24.12 || >=25"
|
"node": "^22.20 || ^24.12 || >=25"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@powersync/common": {
|
||||||
|
"version": "1.57.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@powersync/common/-/common-1.57.3.tgz",
|
||||||
|
"integrity": "sha512-gzXcdfO1EnwXNBITDbnx7oN23d91HjoumS/XzPmVZEwV6S/IBcao/Unv4+aQDvijQVzTtXI+jTl98i9v+TrODQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"js-logger": "^1.6.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@powersync/react": {
|
||||||
|
"version": "1.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@powersync/react/-/react-1.10.0.tgz",
|
||||||
|
"integrity": "sha512-orM29qocbF8zYTBM27j9nPNztyplpbNVaw8djpqxl8N2wESqHTbHOKpatg+Fa5aofgjzmrkiGwevJvCbiWYYWQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@powersync/common": "^1.51.0",
|
||||||
|
"react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@powersync/web": {
|
||||||
|
"version": "1.39.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@powersync/web/-/web-1.39.1.tgz",
|
||||||
|
"integrity": "sha512-mu01OE3sQrulRgvDOWw1xRbRbPhJMOfz5sIVIQ2lENGDkqgWdlRhpkHVfeVi80tCZoP/AFYkQAFRszCgzqFUxQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@powersync/common": "1.57.3",
|
||||||
|
"comlink": "^4.4.2",
|
||||||
|
"commander": "^12.1.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"powersync-web": "bin/powersync.cjs"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@journeyapps/wa-sqlite": "^1.5.0",
|
||||||
|
"@powersync/common": "^1.57.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@powersync/web/node_modules/commander": {
|
||||||
|
"version": "12.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
|
||||||
|
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@prisma/adapter-pg": {
|
"node_modules/@prisma/adapter-pg": {
|
||||||
"version": "7.10.1",
|
"version": "7.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.10.1.tgz",
|
||||||
@@ -3539,6 +3594,12 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/comlink": {
|
||||||
|
"version": "4.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/comlink/-/comlink-4.4.2.tgz",
|
||||||
|
"integrity": "sha512-OxGdvBmJuNKSCMO4NTl1L47VRp6xn2wG4F/2hYzB6tiCb709otOxtEYCSvK80PtjODfXXZu8ds+Nw5kVCjqd2g==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/commander": {
|
"node_modules/commander": {
|
||||||
"version": "13.1.0",
|
"version": "13.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
|
||||||
@@ -4946,6 +5007,12 @@
|
|||||||
"integrity": "sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==",
|
"integrity": "sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/js-logger": {
|
||||||
|
"version": "1.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-logger/-/js-logger-1.6.1.tgz",
|
||||||
|
"integrity": "sha512-yTgMCPXVjhmg28CuUH8CKjU+cIKL/G+zTu4Fn4lQxs8mRFH/03QTNvEFngcxfg/gRDiQAOoyCKmMTOm9ayOzXA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
|
|||||||
@@ -30,6 +30,8 @@
|
|||||||
"publish": "bash scripts/wsl/build-apk.sh release"
|
"publish": "bash scripts/wsl/build-apk.sh release"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@powersync/react": "^1.10.0",
|
||||||
|
"@powersync/web": "^1.39.1",
|
||||||
"@prisma/adapter-pg": "^7.9.1",
|
"@prisma/adapter-pg": "^7.9.1",
|
||||||
"@prisma/client": "^7.7.0",
|
"@prisma/client": "^7.7.0",
|
||||||
"@zythum02/mijia-api": "^0.1.4",
|
"@zythum02/mijia-api": "^0.1.4",
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
// PowerSync 后端连接器: 凭证获取 + 上传队列落地 (0.2.1)
|
||||||
|
// 契约见 server/routes/powersync.js: POST /api/powersync/upload { ops: [{ id, table, op, uuid, data }] }
|
||||||
|
import { PowerSyncBackendConnector, UpdateType } from '@powersync/web'
|
||||||
|
import { api, getToken } from '../../api/client.js'
|
||||||
|
|
||||||
|
export class IBoardConnector extends PowerSyncBackendConnector {
|
||||||
|
async fetchCredentials() {
|
||||||
|
// 未登录: 返回 null 让 SDK 保持断开 (登录后由页面触发 connect)
|
||||||
|
if (!getToken()) return null
|
||||||
|
const { token, endpoint } = await api('/powersync/token', { method: 'POST' })
|
||||||
|
return { endpoint: endpoint || '', token }
|
||||||
|
}
|
||||||
|
|
||||||
|
async invalidateCredentials() {
|
||||||
|
// 凭证不缓存, 每次向业务后端现取
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadData(database) {
|
||||||
|
const batch = await database.getCrudBatch()
|
||||||
|
if (!batch) return
|
||||||
|
|
||||||
|
// 本地行的 id 即为 uuid (客户端创建时生成), 服务端按 uuid 幂等 upsert
|
||||||
|
const ops = batch.crud.map((op, i) => ({
|
||||||
|
id: String(i),
|
||||||
|
table: op.table,
|
||||||
|
op: op.op === UpdateType.DELETE ? 'DELETE' : 'PUT',
|
||||||
|
uuid: op.id,
|
||||||
|
data: op.opData || {},
|
||||||
|
}))
|
||||||
|
|
||||||
|
let res
|
||||||
|
try {
|
||||||
|
res = await api('/powersync/upload', { method: 'POST', body: { ops } })
|
||||||
|
} catch (e) {
|
||||||
|
// 网络失败/401: 抛出让 SDK 稍后重试本批
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
const failed = (res?.results || []).filter((r) => !r.ok)
|
||||||
|
if (failed.length) {
|
||||||
|
// 单条业务错误: 记录并放行 (重试也无法成功, 避免队列卡死); 服务端已尽力落地其余 op
|
||||||
|
console.error('[powersync] upload 部分失败:', failed)
|
||||||
|
}
|
||||||
|
await batch.complete()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
// PowerSync 本地库单例 (0.2.1 离线编辑)
|
||||||
|
// 页面用法: usePowerSyncQuery 等见 src/lib/powersync/react.jsx; 写入用 db.execute/writeTransaction
|
||||||
|
import { PowerSyncDatabase } from '@powersync/web'
|
||||||
|
import { AppSchema } from './schema.js'
|
||||||
|
import { IBoardConnector } from './connector.js'
|
||||||
|
|
||||||
|
let dbInstance = null
|
||||||
|
|
||||||
|
export function getPowersyncDb() {
|
||||||
|
if (!dbInstance) {
|
||||||
|
dbInstance = new PowerSyncDatabase({
|
||||||
|
schema: AppSchema,
|
||||||
|
database: {
|
||||||
|
// 默认 IDBBatchAtomicVFS (IndexedDB), 兼容性最广; WebView2 (Chromium) 后续可切 OPFS 加速
|
||||||
|
dbFilename: 'iboard-powersync.db',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return dbInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 登录后调用: 建立同步连接 (幂等) */
|
||||||
|
export async function connectPowersync() {
|
||||||
|
const db = getPowersyncDb()
|
||||||
|
db.connect(new IBoardConnector())
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 退出登录时调用: 断开同步并清空本地库 */
|
||||||
|
export async function disconnectAndClearPowersync() {
|
||||||
|
if (!dbInstance) return
|
||||||
|
try {
|
||||||
|
await dbInstance.disconnectAndClear()
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
// PowerSync 客户端 schema (0.2.1 离线编辑)
|
||||||
|
// 表名/列名与 deploy/0.2.1/powersync/sync-config.yaml 的输出严格一致 (PascalCase + text 型 id 由 SDK 自建)
|
||||||
|
// 见 docs/V0.2/离线编辑模块/0.2.1实施方案.md
|
||||||
|
import { column, Schema, Table } from '@powersync/web'
|
||||||
|
|
||||||
|
// 时间/JSON 列在同步流里以文本落地, 本地按字符串读写 (JSON 列为 JSON 字符串)
|
||||||
|
const project = new Table(
|
||||||
|
{
|
||||||
|
uuid: column.text,
|
||||||
|
code: column.text,
|
||||||
|
name: column.text,
|
||||||
|
ownerId: column.text,
|
||||||
|
isPersonal: column.integer,
|
||||||
|
archived: column.integer,
|
||||||
|
notes: column.text,
|
||||||
|
taskKinds: column.text,
|
||||||
|
icon: column.text,
|
||||||
|
createdAt: column.text,
|
||||||
|
updatedAt: column.text,
|
||||||
|
},
|
||||||
|
{ indexes: { code: ['code'] } },
|
||||||
|
)
|
||||||
|
|
||||||
|
const projectMember = new Table({
|
||||||
|
uuid: column.text,
|
||||||
|
projectId: column.text,
|
||||||
|
userId: column.text,
|
||||||
|
permission: column.text,
|
||||||
|
createdAt: column.text,
|
||||||
|
})
|
||||||
|
|
||||||
|
const boardColumn = new Table(
|
||||||
|
{
|
||||||
|
uuid: column.text,
|
||||||
|
projectId: column.text,
|
||||||
|
name: column.text,
|
||||||
|
sortOrder: column.integer,
|
||||||
|
boundStatus: column.text,
|
||||||
|
retention: column.text,
|
||||||
|
createdAt: column.text,
|
||||||
|
updatedAt: column.text,
|
||||||
|
},
|
||||||
|
{ indexes: { project: ['projectId'] } },
|
||||||
|
)
|
||||||
|
|
||||||
|
const task = new Table(
|
||||||
|
{
|
||||||
|
uuid: column.text,
|
||||||
|
projectId: column.text,
|
||||||
|
columnId: column.text,
|
||||||
|
parentId: column.text,
|
||||||
|
title: column.text,
|
||||||
|
description: column.text,
|
||||||
|
kind: column.text,
|
||||||
|
markDate: column.text,
|
||||||
|
status: column.text,
|
||||||
|
doneAt: column.text,
|
||||||
|
assigneeId: column.text,
|
||||||
|
creatorId: column.text,
|
||||||
|
extras: column.text,
|
||||||
|
sortOrder: column.integer,
|
||||||
|
number: column.integer,
|
||||||
|
createdAt: column.text,
|
||||||
|
updatedAt: column.text,
|
||||||
|
},
|
||||||
|
{ indexes: { project: ['projectId'], column: ['columnId'], number: ['projectId', 'number'] } },
|
||||||
|
)
|
||||||
|
|
||||||
|
const taskMember = new Table({
|
||||||
|
uuid: column.text,
|
||||||
|
taskId: column.text,
|
||||||
|
userId: column.text,
|
||||||
|
})
|
||||||
|
|
||||||
|
const taskTag = new Table({
|
||||||
|
uuid: column.text,
|
||||||
|
taskId: column.text,
|
||||||
|
name: column.text,
|
||||||
|
})
|
||||||
|
|
||||||
|
const user = new Table({
|
||||||
|
name: column.text,
|
||||||
|
email: column.text,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const AppSchema = new Schema({
|
||||||
|
Project: project,
|
||||||
|
ProjectMember: projectMember,
|
||||||
|
BoardColumn: boardColumn,
|
||||||
|
Task: task,
|
||||||
|
TaskMember: taskMember,
|
||||||
|
TaskTag: taskTag,
|
||||||
|
User: user,
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user