add commitall api and button

This commit is contained in:
monoid 2021-10-13 17:12:03 +09:00
parent e7906dd889
commit 902c845e8a
15 changed files with 165 additions and 68 deletions

1
app.ts
View File

@ -5,6 +5,7 @@ import { getAdminAccessTokenValue,getAdminRefreshTokenValue, accessTokenName, re
import { join } from "path"; import { join } from "path";
import { ipcMain } from 'electron'; import { ipcMain } from 'electron';
import { UserAccessor } from "./src/model/mod"; import { UserAccessor } from "./src/model/mod";
function registerChannel(cntr: UserAccessor){ function registerChannel(cntr: UserAccessor){
ipcMain.handle('reset_password', async(event,username:string,password:string)=>{ ipcMain.handle('reset_password', async(event,username:string,password:string)=>{
const user = await cntr.findUser(username); const user = await cntr.findUser(username);

View File

@ -1,4 +1,4 @@
import Knex from 'knex'; import {Knex} from 'knex';
export async function up(knex:Knex) { export async function up(knex:Knex) {
await knex.schema.createTable("users",(b)=>{ await knex.schema.createTable("users",(b)=>{

View File

@ -25,7 +25,10 @@
{ {
"from": "dist/", "from": "dist/",
"to": "dist/", "to": "dist/",
"filter":["**/*","!**/*.map"] "filter": [
"**/*",
"!**/*.map"
]
}, },
"index.html" "index.html"
], ],
@ -49,24 +52,26 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@louislam/sqlite3": "^6.0.0",
"chokidar": "^3.5.1", "chokidar": "^3.5.1",
"jsonschema": "^1.4.0", "jsonschema": "^1.4.0",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"knex": "^0.21.16", "knex": "^0.95.11",
"koa": "^2.13.1", "koa": "^2.13.1",
"koa-bodyparser": "^4.3.0", "koa-bodyparser": "^4.3.0",
"koa-router": "^10.0.0", "koa-router": "^10.0.0",
"natural-orderby": "^2.0.3", "natural-orderby": "^2.0.3",
"node-stream-zip": "^1.12.0", "node-stream-zip": "^1.12.0",
"sqlite3": "^5.0.1" "sqlite3": "^5.0.2",
"tiny-async-pool": "^1.2.0"
}, },
"devDependencies": { "devDependencies": {
"@types/jsonwebtoken": "^8.5.0", "@types/jsonwebtoken": "^8.5.0",
"@types/knex": "^0.16.1",
"@types/koa": "^2.11.6", "@types/koa": "^2.11.6",
"@types/koa-bodyparser": "^4.3.0", "@types/koa-bodyparser": "^4.3.0",
"@types/koa-router": "^7.4.1", "@types/koa-router": "^7.4.1",
"@types/node": "^14.14.22", "@types/node": "^14.14.22",
"@types/tiny-async-pool": "^1.0.0",
"electron": "^11.2.0", "electron": "^11.2.0",
"electron-builder": "^22.9.1", "electron-builder": "^22.9.1",
"eslint-plugin-node": "^11.1.0", "eslint-plugin-node": "^11.1.0",

View File

@ -23,6 +23,7 @@
"dependencies": { "dependencies": {
"@material-ui/core": "^4.11.2", "@material-ui/core": "^4.11.2",
"@material-ui/icons": "^4.11.2", "@material-ui/icons": "^4.11.2",
"@mui/material": "^5.0.3",
"react": "^17.0.1", "react": "^17.0.1",
"react-dom": "^17.0.1", "react-dom": "^17.0.1",
"react-router-dom": "^5.2.0" "react-router-dom": "^5.2.0"

View File

@ -2,6 +2,7 @@ import React, { useContext, useEffect, useState } from 'react';
import { CommonMenuList, Headline } from "../component/mod"; import { CommonMenuList, Headline } from "../component/mod";
import { UserContext } from "../state"; import { UserContext } from "../state";
import { Box, Grid, Paper, Typography,Button, makeStyles, Theme } from "@material-ui/core"; import { Box, Grid, Paper, Typography,Button, makeStyles, Theme } from "@material-ui/core";
import {Stack} from '@mui/material';
const useStyles = makeStyles((theme:Theme)=>({ const useStyles = makeStyles((theme:Theme)=>({
paper:{ paper:{
@ -26,17 +27,25 @@ type FileDifference = {
function TypeDifference(prop:{ function TypeDifference(prop:{
content:FileDifference, content:FileDifference,
onCommit:(v:{type:string,path:string})=>void onCommit:(v:{type:string,path:string})=>void,
onCommitAll:(type:string) => void
}){ }){
const classes = useStyles(); const classes = useStyles();
const x = prop.content; const x = prop.content;
const [button_disable,set_disable] = useState(false); const [button_disable,set_disable] = useState(false);
return (<Paper className={classes.paper}> return (<Paper className={classes.paper}>
<Typography variant='h3' className={classes.contentTitle}>{x.type}</Typography> <Box className={classes.contentTitle}>
<Typography variant='h3' >{x.type}</Typography>
<Button variant="contained" key={x.type} onClick={()=>{
set_disable(true);
prop.onCommitAll(x.type);
set_disable(false);
}}>Commit all</Button>
</Box>
{x.value.map(y=>( {x.value.map(y=>(
<Box className={classes.commitable} key={y.path}> <Box className={classes.commitable} key={y.path}>
<Button onClick={()=>{ <Button variant="contained" onClick={()=>{
set_disable(true); set_disable(true);
prop.onCommit(y); prop.onCommit(y);
set_disable(false); set_disable(false);
@ -76,6 +85,25 @@ export function DifferencePage(){
if(bb.ok){ if(bb.ok){
doLoad(); doLoad();
} }
else{
console.error("fail to add document");
}
}
const CommitAll = async (type :string)=>{
const res = await fetch("/api/diff/commitall",{
method:"POST",
body: JSON.stringify({type:type}),
headers:{
'content-type':'application/json'
}
});
const bb = await res.json();
if(bb.ok){
doLoad();
}
else{
console.error("fail to add document");
}
} }
useEffect( useEffect(
()=>{ ()=>{
@ -90,7 +118,7 @@ export function DifferencePage(){
return (<Headline menu={menu}> return (<Headline menu={menu}>
{(ctx.username == "admin") ? (<div> {(ctx.username == "admin") ? (<div>
{(diffList.map(x=> {(diffList.map(x=>
<TypeDifference key={x.type} content={x} onCommit={Commit}/>))} <TypeDifference key={x.type} content={x} onCommit={Commit} onCommitAll={CommitAll}/>))}
</div>) </div>)
:(<Typography variant='h2'>Not Allowed : please login as an admin</Typography>) :(<Typography variant='h2'>Not Allowed : please login as an admin</Typography>)
} }

View File

@ -1,5 +1,5 @@
import { Document, DocumentBody, DocumentAccessor, QueryListOption } from '../model/doc'; import { Document, DocumentBody, DocumentAccessor, QueryListOption } from '../model/doc';
import Knex from 'knex'; import {Knex} from 'knex';
import {createKnexTagController} from './tag'; import {createKnexTagController} from './tag';
import { TagAccessor } from '../model/tag'; import { TagAccessor } from '../model/tag';
@ -15,6 +15,43 @@ class KnexDocumentAccessor implements DocumentAccessor{
this.knex = knex; this.knex = knex;
this.tagController = createKnexTagController(knex); this.tagController = createKnexTagController(knex);
} }
async addList(content_list: DocumentBody[]):Promise<number[]>{
return await this.knex.transaction(async (trx)=>{
//add tags
const tagCollected = new Set<string>();
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){ async add(c: DocumentBody){
const {tags,additional, ...rest} = c; const {tags,additional, ...rest} = c;
const id_lst = await this.knex.insert({ const id_lst = await this.knex.insert({

View File

@ -1,5 +1,5 @@
import {Tag, TagAccessor} from '../model/tag'; import {Tag, TagAccessor} from '../model/tag';
import Knex from 'knex'; import {Knex} from 'knex';
type DBTags = { type DBTags = {
name: string, name: string,

View File

@ -1,4 +1,4 @@
import Knex from 'knex'; import {Knex} from 'knex';
import {IUser,UserCreateInput, UserAccessor, Password} from '../model/user'; import {IUser,UserCreateInput, UserAccessor, Password} from '../model/user';
type PermissionTable = { type PermissionTable = {

View File

@ -6,9 +6,12 @@ import {ContentList} from './content_list';
//refactoring needed. //refactoring needed.
export class ContentDiffHandler{ export class ContentDiffHandler{
/** content file list waiting to add */
waiting_list:ContentList; waiting_list:ContentList;
/** deleted contents */
tombstone: Map<string,Document>;//hash, contentfile tombstone: Map<string,Document>;//hash, contentfile
doc_cntr: DocumentAccessor; doc_cntr: DocumentAccessor;
/** content type of handle */
content_type: string; content_type: string;
constructor(cntr: DocumentAccessor,content_type:string){ constructor(cntr: DocumentAccessor,content_type:string){
this.waiting_list = new ContentList(); this.waiting_list = new ContentList();
@ -30,14 +33,19 @@ export class ContentDiffHandler{
private async OnDeleted(cpath: string){ private async OnDeleted(cpath: string){
const basepath = dirname(cpath); const basepath = dirname(cpath);
const filename = basename(cpath); const filename = basename(cpath);
if(this.waiting_list.hasPath(cpath)){ //if it wait to add, delete it from waiting list.
this.waiting_list.deletePath(cpath); if(this.waiting_list.hasByPath(cpath)){
this.waiting_list.deleteByPath(cpath);
return; return;
} }
const dbc = await this.doc_cntr.findByPath(basepath,filename); const dbc = await this.doc_cntr.findByPath(basepath,filename);
if(dbc.length === 0) return; //ignore //when there is no related content in db, ignore.
if(this.waiting_list.hasHash(dbc[0].content_hash)){ if(dbc.length === 0) return;
//if path changed, update changed path. // When a path is changed, it takes into account when the
// creation event occurs first and the deletion occurs, not
// the change event.
if(this.waiting_list.hasByHash(dbc[0].content_hash)){
//if a path is changed, update the changed path.
await this.doc_cntr.update({ await this.doc_cntr.update({
id:dbc[0].id, id:dbc[0].id,
deleted_at: null, deleted_at: null,
@ -46,7 +54,7 @@ export class ContentDiffHandler{
}); });
return; return;
} }
//db invalidate //invalidate db and add it to tombstone.
await this.doc_cntr.update({ await this.doc_cntr.update({
id:dbc[0].id, id:dbc[0].id,
deleted_at: Date.now(), deleted_at: Date.now(),

View File

@ -1,35 +1,25 @@
import { ContentFile } from '../content/mod'; import { ContentFile } from '../content/mod';
import event from 'events';
interface ContentListEvent{ export class ContentList{
'set':(c:ContentFile)=>void, /** path map */
'delete':(c:ContentFile)=>void, private cl:Map<string,ContentFile>;
} /** hash map */
private hl:Map<string,ContentFile>;
export class ContentList extends event.EventEmitter{
cl:Map<string,ContentFile>;
hl:Map<string,ContentFile>;
on<U extends keyof ContentListEvent>(event:U,listener:ContentListEvent[U]): this{
return super.on(event,listener);
}
emit<U extends keyof ContentListEvent>(event:U,...arg:Parameters<ContentListEvent[U]>): boolean{
return super.emit(event,...arg);
}
constructor(){ constructor(){
super();
this.cl = new Map; this.cl = new Map;
this.hl = new Map; this.hl = new Map;
} }
hasHash(s:string){ hasByHash(s:string){
return this.hl.has(s); return this.hl.has(s);
} }
hasPath(p:string){ hasByPath(p:string){
return this.cl.has(p); return this.cl.has(p);
} }
getHash(s:string){ getByHash(s:string){
return this.hl.get(s) return this.hl.get(s)
} }
getPath(p:string){ getByPath(p:string){
return this.cl.get(p); return this.cl.get(p);
} }
async set(c:ContentFile){ async set(c:ContentFile){
@ -37,25 +27,29 @@ export class ContentList extends event.EventEmitter{
const hash = await c.getHash(); const hash = await c.getHash();
this.cl.set(path,c); this.cl.set(path,c);
this.hl.set(hash,c); this.hl.set(hash,c);
this.emit('set',c);
} }
/** delete content file */
async delete(c:ContentFile){ async delete(c:ContentFile){
const hash = await c.getHash();
let r = true; let r = true;
r = this.cl.delete(c.path) && r; r = this.cl.delete(c.path) && r;
r = this.hl.delete(await c.getHash()) && r; r = this.hl.delete(hash) && r;
this.emit('delete',c);
return r; return r;
} }
async deletePath(p:string){ async deleteByPath(p:string){
const o = this.getPath(p); const o = this.getByPath(p);
if(o === undefined) return false; if(o === undefined) return false;
return this.delete(o); return this.delete(o);
} }
async deleteHash(s:string){ async deleteByHash(s:string){
const o = this.getHash(s); const o = this.getByHash(s);
if(o === undefined) return false; if(o === undefined) return false;
return this.delete(o); return this.delete(o);
} }
clear(){
this.cl.clear();
this.hl.clear();
}
getAll(){ getAll(){
return [...this.cl.values()]; return [...this.cl.values()];
} }

View File

@ -1,8 +1,8 @@
import { DocumentAccessor } from '../model/doc'; import { DocumentAccessor } from '../model/doc';
import {ContentDiffHandler} from './content_handler'; import {ContentDiffHandler} from './content_handler';
import { IDiffWatcher } from './watcher'; import { IDiffWatcher } from './watcher';
import asyncPool from 'tiny-async-pool';
//import {join as pathjoin} from 'path';
export class DiffManager{ export class DiffManager{
watching: {[content_type:string]:ContentDiffHandler}; watching: {[content_type:string]:ContentDiffHandler};
doc_cntr: DocumentAccessor; doc_cntr: DocumentAccessor;
@ -19,7 +19,7 @@ export class DiffManager{
} }
async commit(type:string,path:string){ async commit(type:string,path:string){
const list = this.watching[type].waiting_list; const list = this.watching[type].waiting_list;
const c = list.getPath(path); const c = list.getByPath(path);
if(c===undefined){ if(c===undefined){
throw new Error("path is not exist"); throw new Error("path is not exist");
} }
@ -28,6 +28,14 @@ export class DiffManager{
const id = await this.doc_cntr.add(body); const id = await this.doc_cntr.add(body);
return id; return id;
} }
async commitAll(type:string){
const list = this.watching[type].waiting_list;
const contentFiles = list.getAll();
list.clear();
const bodies = await asyncPool(30,contentFiles,async (x)=>await x.createDocumentBody());
const ids = await this.doc_cntr.addList(bodies);
return ids;
}
getAdded(){ getAdded(){
return Object.keys(this.watching).map(x=>({ return Object.keys(this.watching).map(x=>({
type:x, type:x,

View File

@ -32,7 +32,6 @@ function checkPostAddedBody(body: any): body is PostAddedBody{
export const postAdded = (diffmgr:DiffManager) => async (ctx:Router.IRouterContext,next:Koa.Next)=>{ export const postAdded = (diffmgr:DiffManager) => async (ctx:Router.IRouterContext,next:Koa.Next)=>{
const reqbody = ctx.request.body; const reqbody = ctx.request.body;
console.log(reqbody);
if(!checkPostAddedBody(reqbody)){ if(!checkPostAddedBody(reqbody)){
sendError(400,"format exception"); sendError(400,"format exception");
return; return;
@ -45,6 +44,27 @@ export const postAdded = (diffmgr:DiffManager) => async (ctx:Router.IRouterConte
} }
ctx.type = 'json'; ctx.type = 'json';
} }
export const postAddedAll = (diffmgr: DiffManager) => async (ctx:Router.IRouterContext,next:Koa.Next) => {
if (!ctx.is('json')){
sendError(400,"format exception");
return;
}
const reqbody = ctx.request.body as Record<string,unknown>;
if(!("type" in reqbody)){
sendError(400,"format exception: there is no \"type\"");
return;
}
const t = reqbody["type"];
if(typeof t !== "string"){
sendError(400,"format exception: invalid type of \"type\"");
return;
}
await diffmgr.commitAll(t);
ctx.body = {
ok:true
};
ctx.type = 'json';
}
/* /*
export const getNotWatched = (diffmgr : DiffManager)=> (ctx:Router.IRouterContext,next:Koa.Next)=>{ export const getNotWatched = (diffmgr : DiffManager)=> (ctx:Router.IRouterContext,next:Koa.Next)=>{
ctx.body = { ctx.body = {
@ -58,5 +78,6 @@ export function createDiffRouter(diffmgr: DiffManager){
const ret = new Router(); const ret = new Router();
ret.get("/list",AdminOnlyMiddleware,getAdded(diffmgr)); ret.get("/list",AdminOnlyMiddleware,getAdded(diffmgr));
ret.post("/commit",AdminOnlyMiddleware,postAdded(diffmgr)); ret.post("/commit",AdminOnlyMiddleware,postAdded(diffmgr));
ret.post("/commitall",AdminOnlyMiddleware,postAddedAll(diffmgr));
return ret; return ret;
} }

View File

@ -102,6 +102,10 @@ export interface DocumentAccessor{
* add document * add document
*/ */
add:(c:DocumentBody)=>Promise<number>; add:(c:DocumentBody)=>Promise<number>;
/**
* add document list
*/
addList:(content_list:DocumentBody[]) => Promise<number[]>;
/** /**
* delete document * delete document
* @returns if it exists, return true. * @returns if it exists, return true.

View File

@ -71,15 +71,7 @@ const UpdateContentHandler = (controller : DocumentAccessor) => async (ctx: Cont
ctx.body = JSON.stringify(success); ctx.body = JSON.stringify(success);
ctx.type = 'json'; ctx.type = 'json';
} }
/*const CreateContentHandler = (controller : DocumentAccessor) => async (ctx: Context, next: Next) => {
const content_desc = ctx.request.body;
if(!isDocBody(content_desc)){
return sendError(400,"it is not a valid format");
}
const id = await controller.add(content_desc);
ctx.body = JSON.stringify(id);
ctx.type = 'json';
};*/
const AddTagHandler = (controller: DocumentAccessor)=>async (ctx: Context, next: Next)=>{ const AddTagHandler = (controller: DocumentAccessor)=>async (ctx: Context, next: Next)=>{
let tag_name = ctx.params['tag']; let tag_name = ctx.params['tag'];
const num = Number.parseInt(ctx.params['num']); const num = Number.parseInt(ctx.params['num']);

4
src/types/db.d.ts vendored
View File

@ -1,4 +1,4 @@
import Knex from "knex"; import {Knex} from "knex";
declare module "knex" { declare module "knex" {
interface Tables { interface Tables {
@ -31,6 +31,4 @@ declare module "knex" {
name: string; name: string;
}; };
} }
namespace Knex {
}
} }