diff --git a/src/routes/config/check.ts b/src/routes/config/check.ts index 18b44c9..42534d7 100644 --- a/src/routes/config/check.ts +++ b/src/routes/config/check.ts @@ -1,8 +1,8 @@ -import { app } from '@/app.ts'; -import { ConfigModel } from './models/model.ts'; +import { eq, and, inArray } from 'drizzle-orm'; +import { app, db, schema } from '@/app.ts'; import { oss } from '@/app.ts'; import { ConfigOssService } from '@kevisual/oss/services'; -import { Op } from 'sequelize'; +import { nanoid } from 'nanoid'; app .route({ @@ -20,14 +20,12 @@ app }, }); const { list, keys, keyEtagMap } = await configOss.getList(); - const configList = await ConfigModel.findAll({ - where: { - key: { - [Op.in]: keys, - }, - uid: tokenUser.id, - }, - }); + const configList = await db.select() + .from(schema.kvConfig) + .where(and( + inArray(schema.kvConfig.key, keys), + eq(schema.kvConfig.uid, tokenUser.id) + )); const needUpdateList = list.filter((item) => { const key = item.key; const hash = keyEtagMap.get(key); @@ -43,30 +41,33 @@ app const keyETag = keyEtagMap.get(key); const configData = keyDataMap.get(key); if (keyETag && configData) { - const [config, created] = await ConfigModel.findOrCreate({ - where: { - key, - uid: tokenUser.id, - }, - defaults: { + const existing = await db.select() + .from(schema.kvConfig) + .where(and(eq(schema.kvConfig.key, key), eq(schema.kvConfig.uid, tokenUser.id))) + .limit(1); + + let config; + if (existing.length === 0) { + const inserted = await db.insert(schema.kvConfig).values({ + id: nanoid(), key, title: key, description: `从${key}:${keyETag} 同步而来`, uid: tokenUser.id, hash: keyETag, data: configData, - }, - }); - if (!created) { - await config.update( - { + }).returning(); + config = inserted[0]; + } else { + const updated = await db.update(schema.kvConfig) + .set({ hash: keyETag, data: json, - }, - { - fields: ['hash', 'data'], - }, - ); + updatedAt: new Date().toISOString(), + }) + .where(eq(schema.kvConfig.id, existing[0].id)) + .returning(); + config = updated[0]; } updateList.push(config); } diff --git a/src/routes/config/config-key.ts b/src/routes/config/config-key.ts index 34edd99..101e8b9 100644 --- a/src/routes/config/config-key.ts +++ b/src/routes/config/config-key.ts @@ -1,7 +1,8 @@ -import { app } from '@/app.ts'; -import { ConfigModel } from './models/model.ts'; +import { eq, and } from 'drizzle-orm'; +import { app, db, schema } from '@/app.ts'; import { User } from '@/models/user.ts'; import { defaultKeys } from './models/default-keys.ts'; +import { nanoid } from 'nanoid'; app .route({ @@ -27,19 +28,28 @@ app } const defaultConfig = defaultKeys.find((item) => item.key === configKey); - const [config, created] = await ConfigModel.findOrCreate({ - where: { - key: configKey, - uid: tokenUser.id, - }, - defaults: { + const existing = await db.select() + .from(schema.kvConfig) + .where(and( + eq(schema.kvConfig.key, configKey), + eq(schema.kvConfig.uid, tokenUser.id) + )) + .limit(1); + + let config; + if (existing.length === 0) { + const inserted = await db.insert(schema.kvConfig).values({ + id: nanoid(), title: defaultConfig?.key, description: defaultConfig?.description || '', key: configKey, uid: tokenUser.id, data: defaultConfig?.data, - }, - }); + }).returning(); + config = inserted[0]; + } else { + config = existing[0]; + } ctx.body = config; }) diff --git a/src/routes/config/list.ts b/src/routes/config/list.ts index dbed552..5ecc807 100644 --- a/src/routes/config/list.ts +++ b/src/routes/config/list.ts @@ -1,8 +1,9 @@ -import { app } from '@/app.ts'; -import { ConfigModel } from './models/model.ts'; +import { eq, desc, and, inArray } from 'drizzle-orm'; +import { app, db, schema } from '@/app.ts'; import { ShareConfigService } from './services/share.ts'; import { oss } from '@/app.ts'; import { ConfigOssService } from '@kevisual/oss/services'; +import { nanoid } from 'nanoid'; app .route({ @@ -13,12 +14,10 @@ app }) .define(async (ctx) => { const { id } = ctx.state.tokenUser; - const config = await ConfigModel.findAll({ - where: { - uid: id, - }, - order: [['updatedAt', 'DESC']], - }); + const config = await db.select() + .from(schema.kvConfig) + .where(eq(schema.kvConfig.uid, id)) + .orderBy(desc(schema.kvConfig.updatedAt)); ctx.body = { list: config, }; @@ -36,9 +35,10 @@ app const tokernUser = ctx.state.tokenUser; const tuid = tokernUser.id; const { id, data, ...rest } = ctx.query?.data || {}; - let config: ConfigModel; + let config: any; if (id) { - config = await ConfigModel.findByPk(id); + const configs = await db.select().from(schema.kvConfig).where(eq(schema.kvConfig.id, id)).limit(1); + config = configs[0]; let keyIsChange = false; if (rest?.key) { keyIsChange = rest.key !== config?.key; @@ -48,47 +48,57 @@ app } if (keyIsChange) { const key = rest.key; - const keyConfig = await ConfigModel.findOne({ - where: { - key, - uid: tuid, - }, - }); + const keyConfigs = await db.select() + .from(schema.kvConfig) + .where(and(eq(schema.kvConfig.key, key), eq(schema.kvConfig.uid, tuid))) + .limit(1); + const keyConfig = keyConfigs[0]; if (keyConfig && keyConfig.id !== id) { ctx.throw(403, 'key is already exists'); } } - await config.update({ - data: data, - ...rest, - }); - if (config.data?.permission?.share === 'public') { + const updated = await db.update(schema.kvConfig) + .set({ + data: data, + ...rest, + updatedAt: new Date().toISOString(), + }) + .where(eq(schema.kvConfig.id, id)) + .returning(); + config = updated[0]; + if ((config.data as any)?.permission?.share === 'public') { await ShareConfigService.expireShareConfig(config.key, tokernUser.username); } ctx.body = config; } else if (rest?.key) { // id 不存在,key存在,则属于更新,key不能重复 const key = rest.key; - config = await ConfigModel.findOne({ - where: { - key, - uid: tuid, - }, - }); + const configs = await db.select() + .from(schema.kvConfig) + .where(and(eq(schema.kvConfig.key, key), eq(schema.kvConfig.uid, tuid))) + .limit(1); + config = configs[0]; if (config) { - await config.update({ - data: data, - ...rest, - }); + const updated = await db.update(schema.kvConfig) + .set({ + data: data, + ...rest, + updatedAt: new Date().toISOString(), + }) + .where(eq(schema.kvConfig.id, config.id)) + .returning(); + config = updated[0]; ctx.body = config; } else { // 根据key创建一个配置 - config = await ConfigModel.create({ + const inserted = await db.insert(schema.kvConfig).values({ + id: nanoid(), key, ...rest, data: data, uid: tuid, - }); + }).returning(); + config = inserted[0]; ctx.body = config; } } @@ -103,22 +113,25 @@ app const data = config.data; const hash = ossConfig.hash(data); if (config.hash !== hash) { - config.hash = hash; - await config.save({ - fields: ['hash'], - }); + await db.update(schema.kvConfig) + .set({ + hash: hash, + updatedAt: new Date().toISOString(), + }) + .where(eq(schema.kvConfig.id, config.id)); await ossConfig.putJsonObject(key, data); } } if (config) return; // id和key不存在。创建一个新的配置, 而且没有id的 - const newConfig = await ConfigModel.create({ + const newConfig = await db.insert(schema.kvConfig).values({ + id: nanoid(), ...rest, data: data, uid: tuid, - }); - ctx.body = newConfig; + }).returning(); + ctx.body = newConfig[0]; }) .addTo(app); @@ -136,17 +149,17 @@ app if (!id && !key) { ctx.throw(400, 'id or key is required'); } - let config: ConfigModel; + let config: any; if (id) { - config = await ConfigModel.findByPk(id); + const configs = await db.select().from(schema.kvConfig).where(eq(schema.kvConfig.id, id)).limit(1); + config = configs[0]; } if (!config && key) { - config = await ConfigModel.findOne({ - where: { - key, - uid: tuid, - }, - }); + const configs = await db.select() + .from(schema.kvConfig) + .where(and(eq(schema.kvConfig.key, key), eq(schema.kvConfig.uid, tuid))) + .limit(1); + config = configs[0]; } if (!config) { ctx.throw(404, 'config not found'); @@ -171,12 +184,9 @@ app const tuid = tokernUser.id; const { id, key } = ctx.query?.data || {}; if (id || key) { - const search: any = id ? { id } : { key }; - const config = await ConfigModel.findOne({ - where: { - ...search - }, - }); + const search: any = id ? eq(schema.kvConfig.id, id) : eq(schema.kvConfig.key, key); + const configs = await db.select().from(schema.kvConfig).where(search).limit(1); + const config = configs[0]; if (config && config.uid === tuid) { const key = config.key; const ossConfig = ConfigOssService.fromBase({ @@ -190,7 +200,7 @@ app await ossConfig.deleteObject(key); } catch (e) { } } - await config.destroy(); + await db.delete(schema.kvConfig).where(eq(schema.kvConfig.id, config.id)); } else { ctx.throw(403, 'no permission'); } diff --git a/src/routes/config/models/model.ts b/src/routes/config/models/model.ts index 9cfd377..ca861fe 100644 --- a/src/routes/config/models/model.ts +++ b/src/routes/config/models/model.ts @@ -1,7 +1,8 @@ import { useContextKey } from '@kevisual/context'; -import { sequelize } from '../../../modules/sequelize.ts'; -import { DataTypes, Model } from 'sequelize'; import { Permission } from '@kevisual/permission'; +import { eq, and } from 'drizzle-orm'; +import { db, schema } from '../../../app.ts'; +import { nanoid } from 'nanoid'; export interface ConfigData { key?: string; @@ -9,23 +10,24 @@ export interface ConfigData { permission?: Permission; } -export type Config = Partial>; +export type Config = { + id: string; + title: string | null; + description: string | null; + tags: unknown; + key: string | null; + data: unknown; + uid: string | null; + hash: string | null; + createdAt: string; + updatedAt: string; + deletedAt: string | null; +}; /** * 用户配置 */ -export class ConfigModel extends Model { - declare id: string; - declare title: string; - declare description: string; - declare tags: string[]; - /** - * @important 配置key, 默认可以为空,如何设置了,必须要唯一。 - */ - declare key: string; - declare data: ConfigData; // files - declare uid: string; - declare hash: string; +export class ConfigModel { /** * 获取用户配置 * @param key 配置key @@ -35,37 +37,60 @@ export class ConfigModel extends Model { * @returns 配置 */ static async getConfig(key: string, opts: { uid: string; defaultData?: any }) { - const [config, isNew] = await ConfigModel.findOrCreate({ - where: { key, uid: opts.uid }, - defaults: { - key, - title: key, - uid: opts.uid, - data: opts?.defaultData || {}, - }, - }); + const existing = await db.select() + .from(schema.kvConfig) + .where(and(eq(schema.kvConfig.key, key), eq(schema.kvConfig.uid, opts.uid))) + .limit(1); + + if (existing.length > 0) { + return { + config: existing[0], + isNew: false, + }; + } + + const inserted = await db.insert(schema.kvConfig).values({ + id: nanoid(), + key, + title: key, + uid: opts.uid, + data: opts?.defaultData || {}, + }).returning(); + return { - config: config, - isNew, + config: inserted[0], + isNew: true, }; } + static async setConfig(key: string, opts: { uid: string; data: any }) { - let config = await ConfigModel.findOne({ - where: { key, uid: opts.uid }, - }); - if (config) { - config.data = { ...config.data, ...opts.data }; - await config.save(); + const existing = await db.select() + .from(schema.kvConfig) + .where(and(eq(schema.kvConfig.key, key), eq(schema.kvConfig.uid, opts.uid))) + .limit(1); + + if (existing.length > 0) { + const config = existing[0]; + const updated = await db.update(schema.kvConfig) + .set({ + data: { ...(config.data as any || {}), ...opts.data }, + updatedAt: new Date().toISOString(), + }) + .where(eq(schema.kvConfig.id, config.id)) + .returning(); + return updated[0]; } else { - config = await ConfigModel.create({ + const inserted = await db.insert(schema.kvConfig).values({ + id: nanoid(), title: key, key, uid: opts.uid, data: opts.data, - }); + }).returning(); + return inserted[0]; } - return config; } + /** * 获取上传配置 * @param key 配置key @@ -82,7 +107,7 @@ export class ConfigModel extends Model { uid: opts.uid, defaultData: defaultConfig, }); - const data = config.config.data; + const data = config.config.data as any; const prefix = `/${data.key}/${data.version}`; return { config: config.config, @@ -90,6 +115,7 @@ export class ConfigModel extends Model { prefix, }; } + static async setUploadConfig(opts: { uid: string; data: { key?: string; version?: string } }) { const config = await ConfigModel.setConfig('upload.json', { uid: opts.uid, @@ -98,52 +124,5 @@ export class ConfigModel extends Model { return config; } } -ConfigModel.init( - { - id: { - type: DataTypes.UUID, - primaryKey: true, - defaultValue: DataTypes.UUIDV4, - comment: 'id', - }, - title: { - type: DataTypes.TEXT, - defaultValue: '', - }, - key: { - type: DataTypes.TEXT, - defaultValue: '', - }, - description: { - type: DataTypes.TEXT, - defaultValue: '', - }, - tags: { - type: DataTypes.JSONB, - defaultValue: [], - }, - hash: { - type: DataTypes.TEXT, - defaultValue: '', - }, - data: { - type: DataTypes.JSONB, - defaultValue: {}, - }, - uid: { - type: DataTypes.UUID, - allowNull: true, - }, - }, - { - sequelize, - tableName: 'kv_config', - paranoid: true, - }, -); - -// ConfigModel.sync({ alter: true, logging: false }).catch((e) => { -// console.error('ConfigModel sync', e); -// }); useContextKey('ConfigModel', () => ConfigModel); diff --git a/src/routes/config/services/share.ts b/src/routes/config/services/share.ts index ddb300b..3397cbc 100644 --- a/src/routes/config/services/share.ts +++ b/src/routes/config/services/share.ts @@ -1,10 +1,10 @@ -import { ConfigModel, Config } from '../models/model.ts'; +import { Config } from '../models/model.ts'; import { CustomError } from '@kevisual/router'; -import { redis } from '@/app.ts'; -import { User } from '@/models/user.ts'; +import { redis, db, schema } from '@/app.ts'; +import { eq, and } from 'drizzle-orm'; import { UserPermission, UserPermissionOptions } from '@kevisual/permission'; -export class ShareConfigService extends ConfigModel { +export class ShareConfigService { /** * 获取分享的配置 * @param key 配置的key @@ -22,26 +22,30 @@ export class ShareConfigService extends ConfigModel { } const owner = username; if (shareCacheConfig) { - const permission = new UserPermission({ permission: shareCacheConfig?.data?.permission, owner }); + const permission = new UserPermission({ permission: (shareCacheConfig?.data as any)?.permission, owner }); const result = permission.checkPermissionSuccess(options); if (!result.success) { throw new CustomError(403, 'no permission'); } return shareCacheConfig; } - const user = await User.findOne({ - where: { username }, - }); + const users = await db.select() + .from(schema.cfUser) + .where(eq(schema.cfUser.username, username)) + .limit(1); + const user = users[0]; if (!user) { throw new CustomError(404, 'user not found'); } - const config = await ConfigModel.findOne({ - where: { key, uid: user.id }, - }); + const configs = await db.select() + .from(schema.kvConfig) + .where(and(eq(schema.kvConfig.key, key), eq(schema.kvConfig.uid, user.id))) + .limit(1); + const config = configs[0]; if (!config) { throw new CustomError(404, 'config not found'); } - const permission = new UserPermission({ permission: config?.data?.permission, owner }); + const permission = new UserPermission({ permission: (config?.data as any)?.permission, owner }); const result = permission.checkPermissionSuccess(options); if (!result.success) { throw new CustomError(403, 'no permission'); diff --git a/src/routes/config/upload-config.ts b/src/routes/config/upload-config.ts index 6c332a0..b5de6b7 100644 --- a/src/routes/config/upload-config.ts +++ b/src/routes/config/upload-config.ts @@ -13,8 +13,9 @@ app const config = await ConfigModel.getUploadConfig({ uid: tokenUser.id, }); - const key = config?.config?.data?.key || ''; - const version = config?.config?.data?.version || ''; + const data: any = config?.config?.data || {}; + const key = data.key || ''; + const version = data.version || ''; const username = tokenUser.username; const prefix = `${key}/${version}/`; ctx.body = { @@ -35,7 +36,7 @@ app }) .define(async (ctx) => { const { id } = ctx.state.tokenUser; - const data = ctx.query.data || {}; + const data = ctx.query?.data || {}; const { key, version } = data; if (!key && !version) { ctx.throw(400, 'key or version is required'); diff --git a/src/routes/index.ts b/src/routes/index.ts index 6ae719f..b594577 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -10,6 +10,8 @@ import './config/index.ts'; // import './file-listener/index.ts'; +import './mark/index.ts'; + import './light-code/index.ts'; import './ai/index.ts'; diff --git a/src/routes/light-code/list.ts b/src/routes/light-code/list.ts index f6740fb..2bc42e3 100644 --- a/src/routes/light-code/list.ts +++ b/src/routes/light-code/list.ts @@ -1,6 +1,6 @@ import { eq, desc, and, like, or } from 'drizzle-orm'; -import { CustomError } from '@kevisual/router'; import { app, db, schema } from '../../app.ts'; +import { CustomError } from '@kevisual/router'; import { filter } from '@kevisual/js-filter' import { z } from 'zod'; app diff --git a/src/routes/mark/index.ts b/src/routes/mark/index.ts new file mode 100644 index 0000000..366cb31 --- /dev/null +++ b/src/routes/mark/index.ts @@ -0,0 +1 @@ +import './list.ts'; \ No newline at end of file diff --git a/src/routes/mark/list.ts b/src/routes/mark/list.ts new file mode 100644 index 0000000..31884f7 --- /dev/null +++ b/src/routes/mark/list.ts @@ -0,0 +1,308 @@ +import { eq, desc, and, like, or, count, sql } from 'drizzle-orm'; +import { app, db, schema } from '../../app.ts'; +import { MarkServices } from './services/mark.ts'; +import dayjs from 'dayjs'; +import { nanoid } from 'nanoid'; + +app + .route({ + path: 'mark', + key: 'list', + description: 'mark list.', + middleware: ['auth'], + }) + .define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + ctx.body = await MarkServices.getList({ + uid: tokenUser.id, + query: ctx.query, + queryType: 'simple', + }); + }) + .addTo(app); + +app + .route({ + path: 'mark', + key: 'getVersion', + middleware: ['auth'], + }) + .define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + const { id } = ctx.query; + if (id) { + const marks = await db.select().from(schema.microMark).where(eq(schema.microMark.id, id)).limit(1); + const markModel = marks[0]; + if (!markModel) { + ctx.throw(404, 'mark not found'); + } + if (markModel.uid !== tokenUser.id) { + ctx.throw(403, 'no permission'); + } + ctx.body = { + version: Number(markModel.version), + updatedAt: markModel.updatedAt, + createdAt: markModel.createdAt, + id: markModel.id, + }; + } else { + ctx.throw(400, 'id is required'); + // const [markModel, created] = await MarkModel.findOrCreate({ + // where: { + // uid: tokenUser.id, + // puid: tokenUser.uid, + // title: dayjs().format('YYYY-MM-DD'), + // }, + // defaults: { + // title: dayjs().format('YYYY-MM-DD'), + // uid: tokenUser.id, + // markType: 'wallnote', + // tags: ['daily'], + // }, + // }); + // ctx.body = { + // version: Number(markModel.version), + // updatedAt: markModel.updatedAt, + // createdAt: markModel.createdAt, + // id: markModel.id, + // created: created, + // }; + } + }) + .addTo(app); + +app + .route({ + path: 'mark', + key: 'get', + middleware: ['auth'], + }) + .define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + const { id } = ctx.query; + if (id) { + const marks = await db.select().from(schema.microMark).where(eq(schema.microMark.id, id)).limit(1); + const markModel = marks[0]; + if (!markModel) { + ctx.throw(404, 'mark not found'); + } + if (markModel.uid !== tokenUser.id) { + ctx.throw(403, 'no permission'); + } + ctx.body = markModel; + } else { + ctx.throw(400, 'id is required'); + // id 不存在,获取当天的title为 日期的一条数据 + // const [markModel, created] = await MarkModel.findOrCreate({ + // where: { + // uid: tokenUser.id, + // puid: tokenUser.uid, + // title: dayjs().format('YYYY-MM-DD'), + // }, + // defaults: { + // title: dayjs().format('YYYY-MM-DD'), + // uid: tokenUser.id, + // markType: 'wallnote', + // tags: ['daily'], + // uname: tokenUser.username, + // puid: tokenUser.uid, + // version: 1, + // }, + // }); + // ctx.body = markModel; + } + }) + .addTo(app); + +app + .route({ + path: 'mark', + key: 'update', + middleware: ['auth'], + isDebug: true, + }) + .define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + const { id, createdAt, updatedAt, uid: _, puid: _2, uname: _3, data, ...rest } = ctx.query.data || {}; + let markModel: any; + if (id) { + const marks = await db.select().from(schema.microMark).where(eq(schema.microMark.id, id)).limit(1); + markModel = marks[0]; + if (!markModel) { + ctx.throw(404, 'mark not found'); + } + if (markModel.uid !== tokenUser.id) { + ctx.throw(403, 'no permission'); + } + const version = Number(markModel.version) + 1; + const updated = await db.update(schema.microMark) + .set({ + ...rest, + data: { + ...(markModel.data as any || {}), + ...data, + }, + version, + updatedAt: new Date().toISOString(), + }) + .where(eq(schema.microMark.id, id)) + .returning(); + markModel = updated[0]; + } else { + const inserted = await db.insert(schema.microMark).values({ + id: nanoid(), + data: data || {}, + ...rest, + uname: tokenUser.username, + uid: tokenUser.id, + puid: tokenUser.uid, + }).returning(); + markModel = inserted[0]; + } + ctx.body = markModel; + }) + .addTo(app); +app + .route({ + path: 'mark', + key: 'updateNode', + middleware: ['auth'], + }) + .define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + const operate = ctx.query.operate || 'update'; + const { id, node } = ctx.query.data || {}; + const marks = await db.select().from(schema.microMark).where(eq(schema.microMark.id, id)).limit(1); + const markModel = marks[0]; + if (!markModel) { + ctx.throw(404, 'mark not found'); + } + if (markModel.uid !== tokenUser.id) { + ctx.throw(403, 'no permission'); + } + // Update JSON node logic with Drizzle + const currentData = markModel.data as any || {}; + const nodes = currentData.nodes || []; + const nodeIndex = nodes.findIndex((n: any) => n.id === node.id); + + let updatedNodes; + if (operate === 'delete') { + updatedNodes = nodes.filter((n: any) => n.id !== node.id); + } else if (nodeIndex >= 0) { + updatedNodes = [...nodes]; + updatedNodes[nodeIndex] = { ...nodes[nodeIndex], ...node }; + } else { + updatedNodes = [...nodes, node]; + } + + const version = Number(markModel.version) + 1; + const updated = await db.update(schema.microMark) + .set({ + data: { ...currentData, nodes: updatedNodes }, + version, + updatedAt: new Date().toISOString(), + }) + .where(eq(schema.microMark.id, id)) + .returning(); + ctx.body = updated[0]; + }) + .addTo(app); +app + .route({ + path: 'mark', + key: 'updateNodes', + middleware: ['auth'], + }) + .define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + const { id, nodeOperateList } = ctx.query.data || {}; + const marks = await db.select().from(schema.microMark).where(eq(schema.microMark.id, id)).limit(1); + const markModel = marks[0]; + if (!markModel) { + ctx.throw(404, 'mark not found'); + } + if (markModel.uid !== tokenUser.id) { + ctx.throw(403, 'no permission'); + } + if (!nodeOperateList || !Array.isArray(nodeOperateList) || nodeOperateList.length === 0) { + ctx.throw(400, 'nodeOperateList is required'); + } + if (nodeOperateList.some((item: any) => !item.node)) { + ctx.throw(400, 'nodeOperateList node is required'); + } + + // Update multiple JSON nodes logic with Drizzle + const currentData = markModel.data as any || {}; + let nodes = currentData.nodes || []; + + for (const item of nodeOperateList) { + const { node, operate = 'update' } = item; + const nodeIndex = nodes.findIndex((n: any) => n.id === node.id); + + if (operate === 'delete') { + nodes = nodes.filter((n: any) => n.id !== node.id); + } else if (nodeIndex >= 0) { + nodes[nodeIndex] = { ...nodes[nodeIndex], ...node }; + } else { + nodes.push(node); + } + } + + const version = Number(markModel.version) + 1; + const updated = await db.update(schema.microMark) + .set({ + data: { ...currentData, nodes }, + version, + updatedAt: new Date().toISOString(), + }) + .where(eq(schema.microMark.id, id)) + .returning(); + ctx.body = updated[0]; + }) + .addTo(app); + +app + .route({ + path: 'mark', + key: 'delete', + middleware: ['auth'], + }) + .define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + const { id } = ctx.query; + const marks = await db.select().from(schema.microMark).where(eq(schema.microMark.id, id)).limit(1); + const markModel = marks[0]; + if (!markModel) { + ctx.throw(404, 'mark not found'); + } + if (markModel.uid !== tokenUser.id) { + ctx.throw(403, 'no permission'); + } + await db.delete(schema.microMark).where(eq(schema.microMark.id, id)); + ctx.body = markModel; + }) + .addTo(app); + +app + .route({ path: 'mark', key: 'getMenu', description: '获取菜单', middleware: ['auth'] }) + .define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + const [rows, totalResult] = await Promise.all([ + db.select({ + id: schema.microMark.id, + title: schema.microMark.title, + summary: schema.microMark.summary, + tags: schema.microMark.tags, + thumbnail: schema.microMark.thumbnail, + link: schema.microMark.link, + createdAt: schema.microMark.createdAt, + updatedAt: schema.microMark.updatedAt, + }).from(schema.microMark).where(eq(schema.microMark.uid, tokenUser.id)), + db.select({ count: count() }).from(schema.microMark).where(eq(schema.microMark.uid, tokenUser.id)) + ]); + ctx.body = { + list: rows, + total: totalResult[0]?.count || 0, + }; + }) + .addTo(app); diff --git a/src/routes/mark/mark-model.ts b/src/routes/mark/mark-model.ts new file mode 100644 index 0000000..e737479 --- /dev/null +++ b/src/routes/mark/mark-model.ts @@ -0,0 +1,327 @@ +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 markedAt: Date; // 标记时间 + 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, // 对应的minio的文件路径 + 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, + }, + markedAt: { + type: DataTypes.DATE, + allowNull: true, + comment: '标记时间', + }, + 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, tableName = 'micro_mark') => { + const sequelize = await useContextKey('sequelize'); + await MarkMInit({ sequelize, tableName }, sync); +}; diff --git a/src/routes/mark/model.ts b/src/routes/mark/model.ts new file mode 100644 index 0000000..7a66721 --- /dev/null +++ b/src/routes/mark/model.ts @@ -0,0 +1,5 @@ +export * from '@kevisual/code-center-module/src/mark/mark-model.ts'; +import { markModelInit, MarkModel, syncMarkModel } from '@kevisual/code-center-module/src/mark/mark-model.ts'; +export { markModelInit, MarkModel }; + +syncMarkModel({ sync: true, alter: true, logging: false }); diff --git a/src/routes/mark/services/mark.ts b/src/routes/mark/services/mark.ts new file mode 100644 index 0000000..9a67948 --- /dev/null +++ b/src/routes/mark/services/mark.ts @@ -0,0 +1,85 @@ +import { eq, desc, asc, and, like, or, count } from 'drizzle-orm'; +import { app, db, schema } from '../../../app.ts'; + +export class MarkServices { + static getList = async (opts: { + /** 查询用户的 */ + uid?: string; + query?: { + page?: number; + pageSize?: number; + search?: string; + markType?: string; + sort?: string; + }; + /** + * 查询类型 + * simple: 简单查询 默认 + */ + queryType?: string; + }) => { + const { uid, query = {} } = opts; + const { page = 1, pageSize = 999, search, sort = 'DESC' } = query; + + const conditions = []; + if (uid) { + conditions.push(eq(schema.microMark.uid, uid)); + } + if (search) { + conditions.push( + or( + like(schema.microMark.title, `%${search}%`), + like(schema.microMark.summary, `%${search}%`) + ) + ); + } + if (opts.query?.markType) { + conditions.push(eq(schema.microMark.markType, opts.query.markType)); + } + + const whereClause = conditions.length > 0 ? and(...conditions) : undefined; + + const queryType = opts.queryType || 'simple'; + let selectFields: any = {}; + + if (queryType === 'simple') { + // Exclude data, config, cover, description + selectFields = { + id: schema.microMark.id, + title: schema.microMark.title, + tags: schema.microMark.tags, + uname: schema.microMark.uname, + uid: schema.microMark.uid, + createdAt: schema.microMark.createdAt, + updatedAt: schema.microMark.updatedAt, + thumbnail: schema.microMark.thumbnail, + link: schema.microMark.link, + summary: schema.microMark.summary, + markType: schema.microMark.markType, + puid: schema.microMark.puid, + deletedAt: schema.microMark.deletedAt, + version: schema.microMark.version, + fileList: schema.microMark.fileList, + key: schema.microMark.key, + }; + } + + const orderByField = sort === 'ASC' ? asc(schema.microMark.updatedAt) : desc(schema.microMark.updatedAt); + + const [rows, totalResult] = await Promise.all([ + queryType === 'simple' + ? db.select(selectFields).from(schema.microMark).where(whereClause).orderBy(orderByField).limit(pageSize).offset((page - 1) * pageSize) + : db.select().from(schema.microMark).where(whereClause).orderBy(orderByField).limit(pageSize).offset((page - 1) * pageSize), + db.select({ count: count() }).from(schema.microMark).where(whereClause) + ]); + + return { + pagination: { + current: page, + pageSize, + total: totalResult[0]?.count || 0, + }, + list: rows, + }; + }; +}