From b0bd771e3da7653223a204697c5341d2ed304dda Mon Sep 17 00:00:00 2001 From: xion Date: Fri, 27 Jun 2025 00:32:37 +0800 Subject: [PATCH] temp --- package.json | 12 +- src/models/org.ts | 2 +- src/program.ts | 2 +- src/routes-simple/router.ts | 2 +- src/routes/config/models/model.ts | 2 +- src/routes/mark/mark-model.ts | 323 +++++++++++++++++++++++++++++ src/routes/mark/model.ts | 325 +----------------------------- src/scripts/common-redis.ts | 2 +- src/test/create-user-secret.ts | 2 +- src/test/sync-mark.ts | 2 +- 10 files changed, 339 insertions(+), 335 deletions(-) create mode 100644 src/routes/mark/mark-model.ts diff --git a/package.json b/package.json index 80b6e0c..2e07b79 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "commander": "^14.0.0", "ioredis": "^5.6.1", "minio": "^8.0.5", - "pg": "^8.16.1", + "pg": "^8.16.2", "pm2": "^6.0.8", "sequelize": "^6.37.7", "sqlite3": "^5.1.7" @@ -61,7 +61,7 @@ "@kevisual/logger": "^0.0.4", "@kevisual/oss": "workspace:*", "@kevisual/permission": "^0.0.3", - "@kevisual/router": "0.0.22", + "@kevisual/router": "0.0.23", "@kevisual/types": "^0.0.10", "@kevisual/use-config": "^1.0.19", "@rollup/plugin-alias": "^5.1.1", @@ -75,12 +75,12 @@ "@types/formidable": "^3.4.5", "@types/jsonwebtoken": "^9.0.10", "@types/lodash-es": "^4.17.12", - "@types/node": "^24.0.3", + "@types/node": "^24.0.4", "@types/react": "^19.1.8", "@types/semver": "^7.7.0", "@types/uuid": "^10.0.0", "archiver": "^7.0.1", - "concurrently": "^9.1.2", + "concurrently": "^9.2.0", "cross-env": "^7.0.3", "crypto-js": "^4.2.0", "dayjs": "^1.11.13", @@ -94,7 +94,7 @@ "node-fetch": "^3.3.2", "nodemon": "^3.1.10", "p-queue": "^8.1.0", - "pg": "^8.16.1", + "pg": "^8.16.2", "pm2": "^6.0.8", "rimraf": "^6.0.1", "rollup": "^4.44.0", @@ -119,5 +119,5 @@ "picomatch": "^4.0.2" }, "pnpm": {}, - "packageManager": "pnpm@10.12.1" + "packageManager": "pnpm@10.12.3" } \ No newline at end of file diff --git a/src/models/org.ts b/src/models/org.ts index 66bed82..8cbf270 100644 --- a/src/models/org.ts +++ b/src/models/org.ts @@ -1,5 +1,5 @@ // import { DataTypes, Model, Sequelize } from 'sequelize'; -// import { useContextKey } from '@kevisual/use-config/context'; +// import { useContextKey } from '@kevisual/context'; // const sequelize = useContextKey('sequelize'); // export class Org extends Model { // declare id: string; diff --git a/src/program.ts b/src/program.ts index fc46f82..bce5205 100644 --- a/src/program.ts +++ b/src/program.ts @@ -1,5 +1,5 @@ import { program, Command } from 'commander'; -// import { useContextKey } from '@kevisual/use-config/context'; +// import { useContextKey } from '@kevisual/context'; // import * as redisLib from './modules/redis.ts'; // import * as sequelizeLib from './modules/sequelize.ts'; // import * as minioLib from './modules/minio.ts'; diff --git a/src/routes-simple/router.ts b/src/routes-simple/router.ts index a4ed7dd..b834c05 100644 --- a/src/routes-simple/router.ts +++ b/src/routes-simple/router.ts @@ -1,6 +1,6 @@ import { router } from '@/app.ts'; import http from 'http'; -import { useContextKey } from '@kevisual/use-config/context'; +import { useContextKey } from '@kevisual/context'; import { checkAuth, error } from './middleware/auth.ts'; import formidable from 'formidable'; export { router, checkAuth, error }; diff --git a/src/routes/config/models/model.ts b/src/routes/config/models/model.ts index bad57ba..a160eaa 100644 --- a/src/routes/config/models/model.ts +++ b/src/routes/config/models/model.ts @@ -1,4 +1,4 @@ -import { useContextKey } from '@kevisual/use-config/context'; +import { useContextKey } from '@kevisual/context'; import { sequelize } from '../../../modules/sequelize.ts'; import { DataTypes, Model } from 'sequelize'; import { Permission } from '@kevisual/permission'; diff --git a/src/routes/mark/mark-model.ts b/src/routes/mark/mark-model.ts new file mode 100644 index 0000000..dc59226 --- /dev/null +++ b/src/routes/mark/mark-model.ts @@ -0,0 +1,323 @@ + +import { useContextKey } from '@kevisual/context'; +import { nanoid, customAlphabet } from 'nanoid'; +import { DataTypes, Model, ModelAttributes } from 'sequelize'; +import type { Sequelize } from 'sequelize'; +export const random = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'); +export type Mark = Partial>; +export type MarkData = { + md?: string; // markdown + mdList?: string[]; // markdown list + type?: string; // 类型 markdown | json | html | image | video | audio | code | link | file + data?: any; + key?: string; // 文件的名称, 唯一 + push?: boolean; // 是否推送到elasticsearch + pushTime?: Date; // 推送时间 + summary?: string; // 摘要 + nodes?: MarkDataNode[]; // 节点 + [key: string]: any; +}; +export type MarkFile = { + id: string; + name: string; + url: string; + size: number; + type: 'self' | 'data' | 'generate'; // generate为生成文件 + query: string; // 'data.nodes[id].content'; + hash: string; + fileKey: string; // 文件的名称, 唯一 +}; +export type MarkDataNode = { + id?: string; + [key: string]: any; +}; +export type MarkConfig = { + [key: string]: any; +}; +export type MarkAuth = { + [key: string]: any; +}; +/** + * 隐秘内容 + * auth + * config + * + */ +export class MarkModel extends Model { + declare id: string; + declare title: string; // 标题,可以ai生成 + declare description: string; // 描述,可以ai生成 + declare cover: string; // 封面,可以ai生成 + declare thumbnail: string; // 缩略图 + declare key: string; // 文件路径 + declare markType: string; // markdown | json | html | image | video | audio | code | link | file + declare link: string; // 访问链接 + declare tags: string[]; // 标签 + declare summary: string; // 摘要, description的简化版 + declare data: MarkData; // 数据 + + declare uid: string; // 操作用户的id + declare puid: string; // 父级用户的id, 真实用户 + declare config: MarkConfig; // mark属于一定不会暴露的内容。 + + declare fileList: MarkFile[]; // 文件管理 + declare uname: string; // 用户的名称, 或者着别名 + + declare createdAt: Date; + declare updatedAt: Date; + declare version: number; + /** + * 加锁更新data中的node的节点,通过node的id + * @param param0 + */ + static async updateJsonNode(id: string, node: MarkDataNode, opts?: { operate?: 'update' | 'delete'; Model?: any; sequelize?: Sequelize }) { + const sequelize = opts?.sequelize || (await useContextKey('sequelize')); + const transaction = await sequelize.transaction(); // 开启事务 + const operate = opts.operate || 'update'; + const isUpdate = operate === 'update'; + const Model = opts.Model || MarkModel; + try { + // 1. 获取当前的 JSONB 字段值(加锁) + const mark = await Model.findByPk(id, { + transaction, + lock: transaction.LOCK.UPDATE, // 加锁,防止其他事务同时修改 + }); + if (!mark) { + throw new Error('Mark not found'); + } + // 2. 修改特定的数组元素 + const data = mark.data as MarkData; + const items = data.nodes; + if (!node.id) { + node.id = random(12); + } + + // 找到要更新的元素 + const itemIndex = items.findIndex((item) => item.id === node.id); + if (itemIndex === -1) { + isUpdate && items.push(node); + } else { + if (isUpdate) { + items[itemIndex] = node; + } else { + items.splice(itemIndex, 1); + } + } + const version = Number(mark.version) + 1; + // 4. 更新 JSONB 字段 + const result = await mark.update( + { + data: { + ...data, + nodes: items, + }, + version, + }, + { transaction }, + ); + + await transaction.commit(); + return result; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + static async updateJsonNodes(id: string, nodes: { node: MarkDataNode; operate?: 'update' | 'delete' }[], opts?: { Model?: any; sequelize?: Sequelize }) { + const sequelize = opts?.sequelize || (await useContextKey('sequelize')); + const transaction = await sequelize.transaction(); // 开启事务 + const Model = opts?.Model || MarkModel; + try { + const mark = await Model.findByPk(id, { + transaction, + lock: transaction.LOCK.UPDATE, // 加锁,防止其他事务同时修改 + }); + if (!mark) { + throw new Error('Mark not found'); + } + const data = mark.data as MarkData; + const _nodes = data.nodes || []; + // 过滤不在nodes中的节点 + const blankNodes = nodes.filter((node) => !_nodes.find((n) => n.id === node.node.id)).map((node) => node.node); + // 更新或删除节点 + const newNodes = _nodes + .map((node) => { + const nodeOperate = nodes.find((n) => n.node.id === node.id); + if (nodeOperate) { + if (nodeOperate.operate === 'delete') { + return null; + } + return nodeOperate.node; + } + return node; + }) + .filter((node) => node !== null); + const version = Number(mark.version) + 1; + const result = await mark.update( + { + data: { + ...data, + nodes: [...blankNodes, ...newNodes], + }, + version, + }, + { transaction }, + ); + await transaction.commit(); + return result; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + static async updateData(id: string, data: MarkData, opts: { Model?: any; sequelize?: Sequelize }) { + const sequelize = opts.sequelize || (await useContextKey('sequelize')); + const transaction = await sequelize.transaction(); // 开启事务 + const Model = opts.Model || MarkModel; + const mark = await Model.findByPk(id, { + transaction, + lock: transaction.LOCK.UPDATE, // 加锁,防止其他事务同时修改 + }); + if (!mark) { + throw new Error('Mark not found'); + } + const version = Number(mark.version) + 1; + const result = await mark.update( + { + ...mark.data, + ...data, + data: { + ...mark.data, + ...data, + }, + version, + }, + { transaction }, + ); + await transaction.commit(); + return result; + } + static async createNew(data: any, opts: { Model?: any; sequelize?: Sequelize }) { + const sequelize = opts.sequelize || (await useContextKey('sequelize')); + const transaction = await sequelize.transaction(); // 开启事务 + const Model = opts.Model || MarkModel; + const result = await Model.create({ ...data, version: 1 }, { transaction }); + await transaction.commit(); + return result; + } +} +export type MarkInitOpts = { + tableName: string; + sequelize?: Sequelize; + callInit?: (attribute: ModelAttributes) => ModelAttributes; + Model?: T extends typeof MarkModel ? T : typeof MarkModel; +}; +export type Opts = { + sync?: boolean; + alter?: boolean; + logging?: boolean | ((...args: any) => any); + force?: boolean; +}; +export const MarkMInit = async (opts: MarkInitOpts, sync?: Opts) => { + const sequelize = await useContextKey('sequelize'); + opts.sequelize = opts.sequelize || sequelize; + const { callInit, Model, ...optsRest } = opts; + const modelAttribute = { + id: { + type: DataTypes.UUID, + primaryKey: true, + defaultValue: DataTypes.UUIDV4, + comment: 'id', + }, + title: { + type: DataTypes.TEXT, + defaultValue: '', + }, + key: { + type: DataTypes.TEXT, + defaultValue: '', + }, + markType: { + type: DataTypes.TEXT, + defaultValue: 'md', // markdown | json | html | image | video | audio | code | link | file + comment: '类型', + }, + description: { + type: DataTypes.TEXT, + defaultValue: '', + }, + cover: { + type: DataTypes.TEXT, + defaultValue: '', + comment: '封面', + }, + thumbnail: { + type: DataTypes.TEXT, + defaultValue: '', + comment: '缩略图', + }, + link: { + type: DataTypes.TEXT, + defaultValue: '', + comment: '链接', + }, + tags: { + type: DataTypes.JSONB, + defaultValue: [], + }, + summary: { + type: DataTypes.TEXT, + defaultValue: '', + comment: '摘要', + }, + config: { + type: DataTypes.JSONB, + defaultValue: {}, + }, + data: { + type: DataTypes.JSONB, + defaultValue: {}, + }, + fileList: { + type: DataTypes.JSONB, + defaultValue: [], + }, + uname: { + type: DataTypes.STRING, + defaultValue: '', + comment: '用户的名称, 更新后的用户的名称', + }, + version: { + type: DataTypes.INTEGER, // 更新刷新版本,多人协作 + defaultValue: 1, + }, + uid: { + type: DataTypes.UUID, + allowNull: true, + }, + puid: { + type: DataTypes.UUID, + allowNull: true, + }, + }; + const InitModel = Model || MarkModel; + InitModel.init(callInit ? callInit(modelAttribute) : modelAttribute, { + sequelize, + paranoid: true, + ...optsRest, + }); + if (sync && sync.sync) { + const { sync: _, ...rest } = sync; + MarkModel.sync({ alter: true, logging: false, ...rest }).catch((e) => { + console.error('MarkModel sync', e); + }); + } +}; + +export const markModelInit = MarkMInit; + +export const syncMarkModel = async (sync?: Opts) => { + const sequelize = await useContextKey('sequelize'); + await MarkMInit({ sequelize, tableName: 'micro_mark' }, sync); +}; + diff --git a/src/routes/mark/model.ts b/src/routes/mark/model.ts index b91adc4..5e43920 100644 --- a/src/routes/mark/model.ts +++ b/src/routes/mark/model.ts @@ -1,324 +1,5 @@ -import { useContextKey } from '@kevisual/use-config/context'; -import { nanoid, customAlphabet } from 'nanoid'; -import { DataTypes, Model, ModelAttributes } from 'sequelize'; -import type { Sequelize } from 'sequelize'; -export const random = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'); -export type Mark = Partial>; -export type MarkData = { - md?: string; // markdown - mdList?: string[]; // markdown list - type?: string; // 类型 markdown | json | html | image | video | audio | code | link | file - data?: any; - key?: string; // 文件的名称, 唯一 - push?: boolean; // 是否推送到elasticsearch - pushTime?: Date; // 推送时间 - summary?: string; // 摘要 - nodes?: MarkDataNode[]; // 节点 - [key: string]: any; -}; -export type MarkFile = { - id: string; - name: string; - url: string; - size: number; - type: 'self' | 'data' | 'generate'; // generate为生成文件 - query: string; // 'data.nodes[id].content'; - hash: string; - fileKey: string; // 文件的名称, 唯一 -}; -export type MarkDataNode = { - id?: string; - [key: string]: any; -}; -export type MarkConfig = { - [key: string]: any; -}; -export type MarkAuth = { - [key: string]: any; -}; -/** - * 隐秘内容 - * auth - * config - * - */ -export class MarkModel extends Model { - declare id: string; - declare title: string; // 标题,可以ai生成 - declare description: string; // 描述,可以ai生成 - declare cover: string; // 封面,可以ai生成 - declare thumbnail: string; // 缩略图 - declare key: string; // 文件路径 - declare markType: string; // markdown | json | html | image | video | audio | code | link | file - declare link: string; // 访问链接 - declare tags: string[]; // 标签 - declare summary: string; // 摘要, description的简化版 - declare data: MarkData; // 数据 - - declare uid: string; // 操作用户的id - declare puid: string; // 父级用户的id, 真实用户 - declare config: MarkConfig; // mark属于一定不会暴露的内容。 - - declare fileList: MarkFile[]; // 文件管理 - declare uname: string; // 用户的名称, 或者着别名 - - declare createdAt: Date; - declare updatedAt: Date; - declare version: number; - /** - * 加锁更新data中的node的节点,通过node的id - * @param param0 - */ - static async updateJsonNode(id: string, node: MarkDataNode, opts?: { operate?: 'update' | 'delete'; Model?: any; sequelize?: Sequelize }) { - const sequelize = opts?.sequelize || (await useContextKey('sequelize')); - const transaction = await sequelize.transaction(); // 开启事务 - const operate = opts.operate || 'update'; - const isUpdate = operate === 'update'; - const Model = opts.Model || MarkModel; - try { - // 1. 获取当前的 JSONB 字段值(加锁) - const mark = await Model.findByPk(id, { - transaction, - lock: transaction.LOCK.UPDATE, // 加锁,防止其他事务同时修改 - }); - if (!mark) { - throw new Error('Mark not found'); - } - // 2. 修改特定的数组元素 - const data = mark.data as MarkData; - const items = data.nodes; - if (!node.id) { - node.id = random(12); - } - - // 找到要更新的元素 - const itemIndex = items.findIndex((item) => item.id === node.id); - if (itemIndex === -1) { - isUpdate && items.push(node); - } else { - if (isUpdate) { - items[itemIndex] = node; - } else { - items.splice(itemIndex, 1); - } - } - const version = Number(mark.version) + 1; - // 4. 更新 JSONB 字段 - const result = await mark.update( - { - data: { - ...data, - nodes: items, - }, - version, - }, - { transaction }, - ); - - await transaction.commit(); - return result; - } catch (error) { - await transaction.rollback(); - throw error; - } - } - static async updateJsonNodes(id: string, nodes: { node: MarkDataNode; operate?: 'update' | 'delete' }[], opts?: { Model?: any; sequelize?: Sequelize }) { - const sequelize = opts?.sequelize || (await useContextKey('sequelize')); - const transaction = await sequelize.transaction(); // 开启事务 - const Model = opts?.Model || MarkModel; - try { - const mark = await Model.findByPk(id, { - transaction, - lock: transaction.LOCK.UPDATE, // 加锁,防止其他事务同时修改 - }); - if (!mark) { - throw new Error('Mark not found'); - } - const data = mark.data as MarkData; - const _nodes = data.nodes || []; - // 过滤不在nodes中的节点 - const blankNodes = nodes.filter((node) => !_nodes.find((n) => n.id === node.node.id)).map((node) => node.node); - // 更新或删除节点 - const newNodes = _nodes - .map((node) => { - const nodeOperate = nodes.find((n) => n.node.id === node.id); - if (nodeOperate) { - if (nodeOperate.operate === 'delete') { - return null; - } - return nodeOperate.node; - } - return node; - }) - .filter((node) => node !== null); - const version = Number(mark.version) + 1; - const result = await mark.update( - { - data: { - ...data, - nodes: [...blankNodes, ...newNodes], - }, - version, - }, - { transaction }, - ); - await transaction.commit(); - return result; - } catch (error) { - await transaction.rollback(); - throw error; - } - } - static async updateData(id: string, data: MarkData, opts: { Model?: any; sequelize?: Sequelize }) { - const sequelize = opts.sequelize || (await useContextKey('sequelize')); - const transaction = await sequelize.transaction(); // 开启事务 - const Model = opts.Model || MarkModel; - const mark = await Model.findByPk(id, { - transaction, - lock: transaction.LOCK.UPDATE, // 加锁,防止其他事务同时修改 - }); - if (!mark) { - throw new Error('Mark not found'); - } - const version = Number(mark.version) + 1; - const result = await mark.update( - { - ...mark.data, - ...data, - data: { - ...mark.data, - ...data, - }, - version, - }, - { transaction }, - ); - await transaction.commit(); - return result; - } - static async createNew(data: any, opts: { Model?: any; sequelize?: Sequelize }) { - const sequelize = opts.sequelize || (await useContextKey('sequelize')); - const transaction = await sequelize.transaction(); // 开启事务 - const Model = opts.Model || MarkModel; - const result = await Model.create({ ...data, version: 1 }, { transaction }); - await transaction.commit(); - return result; - } -} -export type MarkInitOpts = { - tableName: string; - sequelize?: Sequelize; - callInit?: (attribute: ModelAttributes) => ModelAttributes; - Model?: T; -}; -export type Opts = { - sync?: boolean; - alter?: boolean; - logging?: boolean; - force?: boolean; -}; -export const MarkMInit = async (opts: MarkInitOpts, sync?: Opts) => { - const sequelize = await useContextKey('sequelize'); - opts.sequelize = opts.sequelize || sequelize; - const { callInit, Model, ...optsRest } = opts; - const modelAttribute = { - id: { - type: DataTypes.UUID, - primaryKey: true, - defaultValue: DataTypes.UUIDV4, - comment: 'id', - }, - title: { - type: DataTypes.TEXT, - defaultValue: '', - }, - key: { - type: DataTypes.TEXT, - defaultValue: '', - }, - markType: { - type: DataTypes.TEXT, - defaultValue: 'md', // markdown | json | html | image | video | audio | code | link | file - comment: '类型', - }, - description: { - type: DataTypes.TEXT, - defaultValue: '', - }, - cover: { - type: DataTypes.TEXT, - defaultValue: '', - comment: '封面', - }, - thumbnail: { - type: DataTypes.TEXT, - defaultValue: '', - comment: '缩略图', - }, - link: { - type: DataTypes.TEXT, - defaultValue: '', - comment: '链接', - }, - tags: { - type: DataTypes.JSONB, - defaultValue: [], - }, - summary: { - type: DataTypes.TEXT, - defaultValue: '', - comment: '摘要', - }, - config: { - type: DataTypes.JSONB, - defaultValue: {}, - }, - data: { - type: DataTypes.JSONB, - defaultValue: {}, - }, - fileList: { - type: DataTypes.JSONB, - defaultValue: [], - }, - uname: { - type: DataTypes.STRING, - defaultValue: '', - comment: '用户的名称, 更新后的用户的名称', - }, - version: { - type: DataTypes.INTEGER, // 更新刷新版本,多人协作 - defaultValue: 1, - }, - uid: { - type: DataTypes.UUID, - allowNull: true, - }, - puid: { - type: DataTypes.UUID, - allowNull: true, - }, - }; - const InitModel = Model || MarkModel; - // @ts-ignore - InitModel.init(callInit ? callInit(modelAttribute) : modelAttribute, { - sequelize, - paranoid: true, - ...optsRest, - }); - if (sync && sync.sync) { - const { sync: _, ...rest } = sync; - MarkModel.sync({ alter: true, logging: false, ...rest }).catch((e) => { - console.error('MarkModel sync', e); - }); - } -}; - -export const markModelInit = MarkMInit; - -export const syncMarkModel = async (sync?: Opts) => { - const sequelize = await useContextKey('sequelize'); - await MarkMInit({ sequelize, tableName: 'micro_mark' }, sync); -}; +export * from './mark-model.ts'; +import { markModelInit, MarkModel, syncMarkModel } from './mark-model.ts'; +export { markModelInit, MarkModel }; syncMarkModel({ sync: true, alter: true, logging: false }); diff --git a/src/scripts/common-redis.ts b/src/scripts/common-redis.ts index 3ea0593..133b7f6 100644 --- a/src/scripts/common-redis.ts +++ b/src/scripts/common-redis.ts @@ -1,3 +1,3 @@ import * as redisLib from '../modules/redis.ts'; -import { useContextKey, useContext } from '@kevisual/use-config/context'; +import { useContextKey, useContext } from '@kevisual/context'; export const redis = useContextKey('redis', () => redisLib.redis); diff --git a/src/test/create-user-secret.ts b/src/test/create-user-secret.ts index f7178b2..77af0c2 100644 --- a/src/test/create-user-secret.ts +++ b/src/test/create-user-secret.ts @@ -1,7 +1,7 @@ import { sequelize } from '../modules/sequelize.ts'; import { initUser } from '../scripts/common.ts'; import '../scripts/common-redis.ts'; -import { useContextKey } from '@kevisual/use-config/context'; +import { useContextKey } from '@kevisual/context'; export const main = async () => { const models = await initUser(); diff --git a/src/test/sync-mark.ts b/src/test/sync-mark.ts index 89e31e9..6fb6ad9 100644 --- a/src/test/sync-mark.ts +++ b/src/test/sync-mark.ts @@ -1,4 +1,4 @@ -import { useContextKey } from '@kevisual/use-config/context'; +import { useContextKey } from '@kevisual/context'; import { sequelize } from '../modules/sequelize.ts'; import { MarkModel, syncMarkModel } from '../routes/mark/model.ts'; export const sequelize2 = useContextKey('sequelize', () => sequelize);