import { Document, DocumentBody, DocumentAccessor, QueryListOption } from '../model/doc'; import {Knex} from 'knex'; import {createKnexTagController} from './tag'; import { TagAccessor } from '../model/tag'; type DBTagContentRelation = { doc_id:number, tag_name:string } class KnexDocumentAccessor implements DocumentAccessor{ knex : Knex; tagController: TagAccessor; constructor(knex : Knex){ this.knex = knex; this.tagController = createKnexTagController(knex); } async addList(content_list: DocumentBody[]):Promise{ return await this.knex.transaction(async (trx)=>{ //add tags const tagCollected = new Set(); content_list.map(x=>x.tags).forEach((x)=>{ x.forEach(x=>{ tagCollected.add(x); }); }); const tagCollectPromiseList = []; const tagController = createKnexTagController(trx); for (const it of tagCollected){ const p = tagController.addTag({name:it}); tagCollectPromiseList.push(p); } await Promise.all(tagCollectPromiseList); //add for each contents const ret = []; for (const content of content_list) { const {tags,additional, ...rest} = content; const id_lst = await trx.insert({ additional:JSON.stringify(additional), created_at:Date.now(), ...rest }).into("document"); const id = id_lst[0]; if(tags.length > 0){ await trx.insert(tags.map(y=>({ doc_id:id, tag_name:y }))).into('doc_tag_relation'); } ret.push(id); } return ret; }); } async add(c: DocumentBody){ const {tags,additional, ...rest} = c; const id_lst = await this.knex.insert({ additional:JSON.stringify(additional), created_at:Date.now(), ...rest }).into('document'); const id = id_lst[0]; for (const it of tags) { this.tagController.addTag({name:it}); } if(tags.length > 0){ await this.knex.insert( tags.map(x=>({doc_id:id,tag_name:x})) ).into("doc_tag_relation"); } return id; }; async del(id:number) { if (await this.findById(id) !== undefined){ await this.knex.delete().from("doc_tag_relation").where({doc_id:id}); await this.knex.delete().from("document").where({id:id}); return true; } return false; }; async findById(id:number,tagload?:boolean): Promise{ const s = await this.knex.select("*").from("document").where({id:id}); if(s.length === 0) return undefined; const first = s[0]; let ret_tags:string[] = [] if(tagload === true){ const tags : DBTagContentRelation[] = await this.knex.select("*") .from("doc_tag_relation").where({doc_id:first.id}); ret_tags = tags.map(x=>x.tag_name); } return { ...first, tags:ret_tags, additional: first.additional !== null ? JSON.parse(first.additional) : {}, }; }; async findDeleted(content_type:string){ const s = await this.knex.select("*") .where({content_type:content_type}) .whereNotNull("update_at") .from("document"); return s.map(x=>({ ...x, tags:[], additional:{} })); } async findList(option?:QueryListOption){ option = option ?? {}; const allow_tag = option.allow_tag ?? []; const eager_loading = option.eager_loading ?? true; const limit = option.limit ?? 20; const use_offset = option.use_offset ?? false; const offset = option.offset ?? 0; const word = option.word; const content_type = option.content_type; const cursor = option.cursor; const buildquery = ()=>{ let query = this.knex.select("document.*"); if(allow_tag.length > 0){ query = query.from("doc_tag_relation as tags_0"); query = query.where("tags_0.tag_name","=",allow_tag[0]); for (let index = 1; index < allow_tag.length; index++) { const element = allow_tag[index]; query = query.innerJoin(`doc_tag_relation as tags_${index}`,`tags_${index}.doc_id`,"tags_0.doc_id"); query = query.where(`tags_${index}.tag_name`,'=',element); } query = query.innerJoin("document","tags_0.doc_id","document.id"); } else{ query = query.from("document"); } if(word !== undefined){ //don't worry about sql injection. query = query.where('title','like',`%${word}%`); } if(content_type !== undefined){ query = query.where('content_type','=',content_type); } if(use_offset){ query = query.offset(offset); } else{ if(cursor !== undefined){ query = query.where('id','<',cursor); } } query = query.limit(limit); query = query.orderBy('id',"desc"); return query; } let query = buildquery(); //console.log(query.toSQL()); let result:Document[] = await query; for(let i of result){ i.additional = JSON.parse((i.additional as unknown) as string); } if(eager_loading){ let idmap: {[index:number]:Document} = {}; for(const r of result){ idmap[r.id] = r; r.tags = []; } let subquery = buildquery(); let tagquery= this.knex.select("id","doc_tag_relation.tag_name").from(subquery) .innerJoin("doc_tag_relation","doc_tag_relation.doc_id","id"); //console.log(tagquery.toSQL()); let tagresult:{id:number,tag_name:string}[] = await tagquery; for(const {id,tag_name} of tagresult){ idmap[id].tags.push(tag_name); } } else{ result.forEach(v=>{v.tags = [];}); } return result; }; async findByPath(path:string,filename?:string):Promise{ const e = filename == undefined ? {} : {filename:filename} const results = await this.knex.select("*").from("document").where({basepath:path,...e}); return results.map(x=>({ ...x, tags:[], additional:{} })) } async update(c:Partial & { id:number }){ const {id,tags,...rest} = c; if (await this.findById(id) !== undefined){ await this.knex.update(rest).where({id: id}).from("document"); return true; } return false; } async addTag(c: Document,tag_name:string){ if (c.tags.includes(tag_name)) return false; this.tagController.addTag({name:tag_name}); await this.knex.insert({tag_name: tag_name, doc_id: c.id}) .into("doc_tag_relation"); c.tags.push(tag_name); return true; } async delTag(c: Document,tag_name:string){ if (c.tags.includes(tag_name)) return false; await this.knex.delete().where({tag_name: tag_name,doc_id: c.id}).from("doc_tag_relation"); c.tags.push(tag_name); return true; } } export const createKnexDocumentAccessor = (knex:Knex): DocumentAccessor=>{ return new KnexDocumentAccessor(knex); }