@@ -1,327 +0,0 @@
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 < InstanceType < typeof MarkModel > > ;
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 < T = any > = {
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 < T = any > ( opts : MarkInitOpts < T > , 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 ) ;
} ;