Compare commits

..

No commits in common. "6ac7ba8fd04d0b0b15eb2d73377397c6853e79f6" and "9b4e482e3a40f3f020e31c5160b0559ba5b84cf1" have entirely different histories.

21 changed files with 118 additions and 962 deletions

View File

@ -1,249 +0,0 @@
package entry
import (
"fmt"
"regexp"
"strings"
"time"
"code.chrissexton.org/cws/cabinet/db"
"github.com/jmoiron/sqlx"
"github.com/rs/zerolog/log"
)
type Entry struct {
db *db.Database
ID int64
Slug string
Content string
Tags []string
Created time.Time
Updated time.Time
AuthorID int64 `db:"author_id"`
}
func PrepareTable(tx *sqlx.Tx) error {
q := `create table if not exists entries (
id integer primary key,
slug text unique not null,
content text not null,
created datetime not null,
updated datetime not null,
author_id integer
)`
_, err := tx.Exec(q)
if err != nil {
tx.Rollback()
return err
}
q = `create table if not exists tags (
id integer primary key,
name text not null,
entry_id integer,
foreign key(entry_id) references entries(id),
constraint unique_name_id unique (name, entry_id)
)`
_, err = tx.Exec(q)
if err != nil {
tx.Rollback()
return err
}
return nil
}
func New(db *db.Database) Entry {
return Entry{
db: db,
ID: -1,
Created: time.Now(),
Updated: time.Now(),
}
}
func GetBySlug(db *db.Database, slug string) (Entry, error) {
e := Entry{db: db}
q := `select * from entries where slug = ?`
if err := db.Get(&e, q, slug); err != nil {
return e, err
}
return e, e.populateTags()
}
func GetByID(db *db.Database, id int64) (Entry, error) {
e := Entry{db: db}
q := `select * from entries where id = ?`
if err := db.Get(&e, q, id); err != nil {
return e, err
}
return e, e.populateTags()
}
func Search(db *db.Database, query string) ([]*Entry, error) {
entries := []*Entry{}
if query != "" {
q := `select * from entries where content like '%?%'`
err := db.Select(&entries, q, query)
if err != nil {
return nil, err
}
} else {
q := `select * from entries`
err := db.Select(&entries, q)
if err != nil {
return nil, err
}
}
for _, e := range entries {
e.db = db
e.populateTags()
}
return entries, nil
}
func RemoveBySlug(db *db.Database, slug string) error {
tx, err := db.Begin()
if err != nil {
return err
}
e, err := GetBySlug(db, slug)
if err != nil {
return err
}
q := `delete from tags where entry_id = ?`
if _, err := tx.Exec(q, e.ID); err != nil {
tx.Rollback()
return err
}
q = `delete from entries where entry_id = ?`
if _, err := tx.Exec(q, e.ID); err != nil {
tx.Rollback()
return err
}
return nil
}
func (e *Entry) populateTags() error {
q := `select name from tags where entry_id = ?`
err := e.db.Select(&e.Tags, q, e.ID)
log.Debug().Interface("entry", e).Msg("populating tags")
return err
}
func (e *Entry) addTag(tag string) error {
q := `insert into tags (name,entry_id) values (?,?)`
_, err := e.db.Exec(q, tag, e.ID)
return err
}
func (e *Entry) removeTag(tag string) error {
q := `delete from tags where name=? and entry_id=?`
_, err := e.db.Exec(q, tag, e.ID)
return err
}
func (e *Entry) UniqueSlug() string {
candidate := strings.Split(e.Content, "\n")[0]
candidateNumber := 0
r := regexp.MustCompile(`[^a-zA-Z0-9 -]`)
candidate = r.ReplaceAllString(candidate, "")
candidate = strings.TrimSpace(candidate)
candidate = strings.ReplaceAll(candidate, " ", "-")
if len(candidate) == 0 {
candidate = "untitled"
}
candidate = strings.ToLower(candidate)
q := `select slug from entries where slug like ?`
slugs := []string{}
if err := e.db.Select(&slugs, q, candidate+"%"); err != nil {
log.Debug().Err(err).Msgf("Could not get candidate slugs: %s", err)
return candidate
}
contains := func(s string, ss []string) bool {
for _, e := range ss {
if s == e {
return true
}
}
return false
}
tmpCandidate := candidate
for contains(tmpCandidate, slugs) {
candidateNumber++
tmpCandidate = fmt.Sprintf("%s-%d", candidate, candidateNumber)
}
return tmpCandidate
}
func (e *Entry) Update() error {
if e.ID == -1 {
return e.Create()
}
old, err := GetByID(e.db, e.ID)
if err != nil {
return err
}
e.Slug = e.UniqueSlug()
q := `update entries set slug=?, content=?, updated=?, author_id=? where id=?`
if _, err = e.db.Exec(q, e.Slug, e.Content, e.Updated.Unix(), e.AuthorID, e.ID); err != nil {
return err
}
for _, t := range e.Tags {
if !contains(old.Tags, t) {
e.addTag(t)
}
}
for _, t := range old.Tags {
if !contains(e.Tags, t) {
e.removeTag(t)
}
}
return nil
}
func contains(items []string, entry string) bool {
for _, e := range items {
if e == entry {
return true
}
}
return false
}
func (e *Entry) Create() error {
if e.ID != -1 {
return e.Update()
}
tx, err := e.db.Begin()
if err != nil {
return err
}
q := `insert into entries (slug,content,created,updated,author_id)
values (?,?,?,?,?)`
res, err := tx.Exec(q, e.Slug, e.Content, e.Created.Unix(), e.Updated.Unix(), e.AuthorID)
if err != nil {
return err
}
e.ID, err = res.LastInsertId()
if err != nil {
tx.Rollback()
e.ID = -1
return err
}
for _, t := range e.Tags {
q = `insert into tags (name,entry_id) values (?,?)`
_, err = tx.Exec(q, t, e.ID)
if err != nil {
tx.Rollback()
return err
}
}
tx.Commit()
return nil
}

View File

@ -8,17 +8,14 @@
"lint": "vue-cli-service lint"
},
"dependencies": {
"asciidoctor": "^2.0.3",
"axios": "^0.19.0",
"bootstrap": "^4.3.1",
"bootstrap-vue": "^2.0.4",
"brace": "latest",
"core-js": "^3.3.2",
"lodash": "^4.17.15",
"vue": "^2.6.10",
"vue-router": "^3.1.3",
"vue2-ace-editor": "^0.0.15",
"vuex": "^3.0.1"
"vuex": "^3.0.1",
"brace": "latest"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.0.0",

View File

@ -1,14 +1,13 @@
<template>
<div id="app">
<b-navbar type="dark" variant="dark">
<b-navbar-brand>🗄 Cabinet</b-navbar-brand>
<b-navbar-brand>Cabinet</b-navbar-brand>
<b-navbar-nav>
<b-nav-item to="/">Home</b-nav-item>
<b-nav-item to="/console">Console</b-nav-item>
<b-nav-item to="/about">About</b-nav-item>
</b-navbar-nav>
</b-navbar>
<Error/>
<router-view/>
</div>
</template>
@ -34,14 +33,3 @@
color: #42b983;
}
</style>
<script>
import Error from "./components/Error";
export default {
name: 'app',
components: {
Error
}
}
</script>

View File

@ -1,6 +1,6 @@
<template>
<div>
<editor ref="myEditor" v-model="text" @init="editorInit" lang="asciidoc" theme="github" width="100%" height="500" />
<editor v-model="text" @init="editorInit" lang="asciidoc" theme="github" width="100%" height="500" />
</div>
</template>
@ -20,13 +20,7 @@
},
watch: {
content: function(newValue) {
console.log('text update\n'+newValue)
let editor = this.$refs.myEditor.editor
editor.renderer.updateFull()
this.text = newValue
},
text: function() {
this.$emit('change', this.text)
}
},
methods: {

View File

@ -1,25 +0,0 @@
<template>
<div>
<b-alert
v-for="(e, i) in errs"
v-bind:key="i"
variant="danger"
dismissible
show="true"
v-show="errs.length > 0">{{e}}</b-alert>
</div>
</template>
<script>
import {mapState} from 'vuex';
export default {
name: "Error",
computed: mapState({
errs: state => state.errs
}),
}
</script>
<style scoped>
</style>

View File

@ -3,13 +3,20 @@
<b-container fluid>
<b-row>
<b-col>
<Editor :content="content" @change="updateContent"/>
<Editor :content="content" />
</b-col>
</b-row>
<b-row>
<b-col cols="1"><label style="padding-top: 0.5em" for="tagList">Tags</label></b-col>
<b-col cols="10">
<TagList id="tagList" :tags="tags" @change="tagUpdate"/>
<label for="tagList">Tags</label>
</b-col>
</b-row>
<b-row>
<b-col cols="10">
<TagList id="tagList" :tags="tags" />
</b-col>
<b-col cols="1">
<b-button variant="primary">Save</b-button>
</b-col>
</b-row>
</b-container>
@ -17,10 +24,8 @@
</template>
<script>
import Editor from "./Editor"
import TagList from "./TagList"
import _ from 'lodash'
import Editor from "./Editor";
import TagList from "./TagList";
export default {
name: "MainEditor",
props: {
@ -30,18 +35,23 @@
if (this.$props.slug) {
this.getFile(this.$props.slug)
}
this.save = _.debounce(this.save, 2000)
},
data: function () {
return {
file: ''
}
},
computed: {
tags: function () {
if (this.$store.state.file) {
return this.$store.state.file.Tags
tags: function() {
if (this.file) {
return this.file.tags
}
return []
},
content: function () {
if (this.$store.state.file) {
return this.$store.state.file.Content
console.log('file:' + this.file)
if (this.file) {
return this.file.contents
}
return "= Main Editor"
}
@ -52,35 +62,10 @@
}
},
methods: {
getFile: function (slug) {
getFile: function(slug) {
this.$store.dispatch('getFile', slug)
},
tagUpdate: function(newTags) {
if (JSON.stringify(newTags) === JSON.stringify(this.file.Tags))
return
this.file.Tags = newTags
this.$emit('markDirty', true)
this.save()
},
updateContent: function (newContent) {
if (this.$store.state.file.Content === newContent)
return
this.$store.state.file.Content = newContent
this.$emit('markDirty', true)
this.save()
},
save: function () {
this.$store.state.file.Content = this.content
console.log("Saving file: " + this.file.Slug)
this.$store.dispatch('saveFile', this.file)
.then(res => {
this.$emit('markDirty', false)
this.$store.dispatch('updateSearch')
if (res.data.Slug != this.$route.params.slug)
this.$router.replace({params: { slug: res.data.Slug }})
})
.catch(err => {
console.log('err:' + err)
.then(file => {
this.file = file
})
}
},

View File

@ -1,71 +0,0 @@
<template>
<div :hidden="!content">
<b-container fluid>
<b-row>
<b-col>
<Viewer :content="content" />
</b-col>
</b-row>
<b-row>
<b-col cols="10">
<label for="tagList" >Tags</label>
</b-col>
</b-row>
<b-row>
<b-col cols="10">
<TagList id="tagList" :tags="tags" :readOnly="true" />
</b-col>
</b-row>
</b-container>
</div>
</template>
<script>
import Viewer from "./Viewer";
import TagList from "./TagList";
export default {
name: "MainView",
props: {
slug: String
},
created() {
if (this.$props.slug) {
this.getFile(this.$props.slug)
}
},
data: function () {
return {
file: ''
}
},
computed: {
tags: function() {
if (this.$store.state.file) {
return this.$store.state.file.Tags
}
return []
},
content: function () {
if (this.$store.state.file) {
return this.$store.state.file.Content
}
return null
}
},
watch: {
slug: function (newValue) {
this.getFile(newValue)
}
},
methods: {
getFile: function(slug) {
this.$store.dispatch('getFile', slug)
}
},
components: {TagList, Viewer}
}
</script>
<style scoped>
</style>

View File

@ -1,53 +0,0 @@
<template>
<b-container fluid>
<b-row>
<b-input placeholder="Search" @update="runQuery" v-model="queryText" />
</b-row>
<b-row v-for="item in results" v-bind:key="item.ID">
<b-col>
<b-link
:to="{ name: target, params: { slug: item.Slug } }"
>{{item.Slug}}</b-link>
</b-col>
</b-row>
</b-container>
</template>
<script>
const _ = require('lodash');
export default {
name: "SearchResults",
data: function() {
return {
queryText: null
}
},
props: {
target: String,
query: String
},
watch: {
query: function(newValue) {
this.queryText = newValue
}
},
computed: {
results: function() {
console.log("results:"+this.$store.state.searchResults)
return this.$store.state.searchResults
}
},
created() {
this.getResults()
this.runQuery = _.debounce(this.runQuery, 1000)
},
methods: {
getResults: function () {
this.$store.dispatch('getSearchResults', null)
},
runQuery: function() {
this.$store.dispatch('getSearchResults', this.query)
}
}
}
</script>

View File

@ -0,0 +1,35 @@
<template>
<b-container fluid>
<b-row v-for="item in results" v-bind:key="item.id">
<b-col>
<b-link
:to="{ name: 'console-slug', params: { slug: item.slug } }"
>{{item.title}}</b-link>
</b-col>
</b-row>
</b-container>
</template>
<script>
export default {
name: "SearchResults",
data: function() {
return {
results: []
}
},
created() {
this.getResults()
},
methods: {
getResults: function () {
console.log('getResults')
this.$store.dispatch('getSearchResults')
.then(res => {
console.log('then'+res)
this.results = res
})
}
}
}
</script>

View File

@ -1,47 +1,21 @@
<template>
<div>
<b-input type="text" placeholder="tags" :value="tagString" @update="changed" :hidden="readOnly"/>
<div :hidden="!readOnly">
<b-badge pill v-for="(tag, idx) in tags" v-bind:key="idx" class="tag">
{{tag}}
</b-badge>
</div>
</div>
<b-input type="text" placeholder="tags" :value="tagString"/>
</template>
<script>
export default {
name: "TagList",
data: function () {
return {
internalTags: []
}
},
props: {
tags: Array,
readOnly: Boolean
},
watch: {
tags: function (newValue) {
this.internalTags = newValue
}
tags: Array
},
computed: {
tagString: function () {
return this.internalTags.join(',')
}
},
methods: {
changed: function (value) {
this.internalTags = value.split(',')
this.$emit('change', this.internalTags)
tagString: function() {
return this.tags.join(' ')
}
}
}
</script>
<style scoped>
.tag {
margin-right: 0.5em;
}
</style>

View File

@ -1,30 +0,0 @@
<template>
<div v-html="adoc">
</div>
</template>
<script>
let asciidoctor = require('asciidoctor')()
export default {
name: "Viewer",
props: {
content: String
},
computed: {
adoc: function () {
if (!this.content)
return ''
let doc = asciidoctor.convert(this.content, {
'safe': 'safe',
'attributes': {'showtitle': true, 'icons': 'font'}
})
return doc
}
}
}
</script>
<style scoped>
</style>

View File

@ -7,20 +7,10 @@ Vue.use(VueRouter)
const routes = [
{
path: '/view/:slug',
name: 'home-slug',
path: '/',
name: 'home',
component: Home
},
{
path: '/search/:query',
name: 'home-search',
component: Home
},
{
path: '/console/search/:query',
name: 'console-search',
component: Console
},
{
path: '/console/:slug',
name: 'console-slug',
@ -38,11 +28,6 @@ const routes = [
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
},
{
path: '/',
name: 'home',
component: Home
}
]

View File

@ -1,79 +1,37 @@
import Vue from 'vue'
import Vuex from 'vuex'
// import _ from 'lodash'
import axios from 'axios'
Vue.use(Vuex)
// function getRandomInt(min, max) {
// min = Math.ceil(min)
// max = Math.floor(max)
// return Math.floor(Math.random() * (max - min + 1)) + min
// }
function getRandomInt(min, max) {
min = Math.ceil(min)
max = Math.floor(max)
return Math.floor(Math.random() * (max - min + 1)) + min
}
// var files = {
// 'test-1': {id: 0, title: 'test 1', contents: '= test 1', slug: 'test-1', tags: ['a']},
// 'test-2': {id: 1, title: 'test 2', contents: '= test 2', slug: 'test-2', tags: ['b']},
// 'test-3': {id: 2, title: 'test 3', contents: '= test 3', slug: 'test-3', tags: ['a','b']},
// 'test-4': {id: 3, title: 'test 4', contents: '= test 4', slug: 'test-4', tags: ['c']}
// }
var files = {
'test-1': {id: 0, title: 'test 1', contents: '= test 1', slug: 'test-1', tags: ['a']},
'test-2': {id: 1, title: 'test 2', contents: '= test 2', slug: 'test-2', tags: ['b']},
'test-3': {id: 2, title: 'test 3', contents: '= test 3', slug: 'test-3', tags: ['a','b']},
'test-4': {id: 3, title: 'test 4', contents: '= test 4', slug: 'test-4', tags: ['c']}
}
export default new Vuex.Store({
state: {
errs: [],
searchResults: [],
query: null,
file: null
},
mutations: {
clearError(state) {
state.errs = []
},
addError(state, err) {
state.errs.push(err)
},
setResults(state, results) {
state.searchResults = results
},
setQuery(state, query) {
state.query = query
},
setFile(state, file) {
state.file = file
}
},
actions: {
getFile: function({ commit }, slug) {
if (slug)
return axios.get('/v1/entries/'+slug)
.catch(err => commit('addError', err))
.then(res => {
commit('setFile', res.data)
})
getFile: function(_, slug) {
console.log('getFile('+slug+')')
return new Promise(function (resolve) {
setTimeout(() => resolve(files[slug]), getRandomInt(0, 1000))
})
},
getSearchResults: function ({dispatch, commit}, query) {
commit('setQuery', query)
dispatch('updateSearch')
},
updateSearch: function ({commit, state}) {
let query = state.query
if (query) {
axios.get('/v1/entries?query='+query)
.catch(err => state.addError(err))
.then(res =>{
console.log("getSearchResults:"+res.data)
commit('setResults', res.data)
})
}
axios.get('/v1/entries')
.catch(err => state.addError(err))
.then(res =>{
console.log("getSearchResults:"+res.data)
commit('setResults', res.data)
})
},
saveFile: function(state, file) {
return axios.put('/v1/entries/'+file.Slug, file)
getSearchResults: function () {
return new Promise(function (resolve) {
setTimeout(() => resolve(Object.values(files)), getRandomInt(0, 1000))
})
}
},
modules: {

View File

@ -1,31 +1,22 @@
<template>
<b-container fluid>
<b-row>
<b-col>
<h1>Console</h1>
</b-col>
</b-row>
<b-row>
<b-col md="5">
<h2>Scratchpad</h2>
<ScratchPad />
</b-col>
<b-col md="5">
<div>
<b-tabs content-class="mt-3">
<b-tab active>
<template v-slot:title>
Editor <span v-bind:class="{ dirty: isDirty, clean: !isDirty }">&bull;</span>
</template>
<MainEditor :slug="$route.params.slug" @markDirty="markDirty"/>
</b-tab>
<b-tab title="Viewer">
<MainView :slug="$route.params.slug" />
</b-tab>
</b-tabs>
</div>
<h2>Editor</h2>
<MainEditor :slug="$route.params.slug" />
</b-col>
<b-col md="2">
<h2>Search Results</h2>
<SearchResults target="console-slug" :query="query"/>
<SearchResults />
</b-col>
</b-row>
</b-container>
@ -34,63 +25,15 @@
<script>
// @ is an alias to /src
import MainEditor from "../components/MainEditor";
import MainView from "../components/MainView";
import ScratchPad from "../components/ScratchPad";
import SearchResults from "../components/Search";
import SearchResults from "../components/SearchResults";
export default {
name: 'home',
components: {
SearchResults,
ScratchPad,
MainEditor,
MainView
},
data: function() {
return {
isDirty: false,
query: ''
}
},
provide: {
update: function() {}
},
methods: {
markDirty: function(dirty) {
console.log('markDirty:'+dirty)
this.isDirty = dirty
}
},
beforeRouteEnter (to, from, next) {
// called before the route that renders this component is confirmed.
// does NOT have access to `this` component instance,
// because it has not been created yet when this guard is called!
console.log('beforeRouteEnter'+to+from)
next()
},
beforeRouteUpdate (to, from, next) {
// called when the route that renders this component has changed,
// but this component is reused in the new route.
// For example, for a route with dynamic params `/foo/:id`, when we
// navigate between `/foo/1` and `/foo/2`, the same `Foo` component instance
// will be reused, and this hook will be called when that happens.
// has access to `this` component instance.
if (this.isDirty) {
const answer = window.confirm('Do you really want to leave? you have unsaved changes!')
if (answer) {
next()
} else {
next(false)
}
}
next()
},
beforeRouteLeave (to, from, next) {
// called when the route that renders this component is about to
// be navigated away from.
// has access to `this` component instance.
console.log('beforeRouteLeave'+to+from)
next()
MainEditor
}
}
</script>
@ -99,10 +42,4 @@ export default {
h2 {
font-size: large;
}
.dirty {
color: red;
}
.clean {
display: none;
}
</style>

View File

@ -1,35 +1,18 @@
<template>
<b-container fluid>
<b-row><b-col>
<h1>Home</h1>
</b-col></b-row>
<b-row>
<b-col md="2">
<SearchResults target="home-slug" :query="$route.params.query" />
</b-col>
<b-col>
<MainView :slug="$route.params.slug" />
</b-col>
</b-row>
</b-container>
<div class="home">
<img alt="Vue logo" src="../assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/>
</div>
</template>
<script>
import SearchResults from '../components/Search.vue'
import MainView from '../components/MainView.vue'
// @ is an alias to /src
import HelloWorld from '@/components/HelloWorld.vue'
export default {
name: 'home',
components: {
SearchResults,
MainView
HelloWorld
}
}
</script>
<style scoped>
h2 {
font-size: large;
}
</style>

View File

@ -2,21 +2,6 @@
# yarn lockfile v1
"@asciidoctor/cli@2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@asciidoctor/cli/-/cli-2.0.1.tgz#5e0154ff06da21aedb1840bff5d49ca6ee7d520d"
integrity sha512-XNqnHPMVTRMDlKk6EEiA61P0WImHqOecoky1p8HIek+0u1jbKbiDku8WraC2EaNy8IWyEjfrdnrDYGvogBVqzA==
dependencies:
yargs "13.2.2"
"@asciidoctor/core@2.0.3":
version "2.0.3"
resolved "https://registry.yarnpkg.com/@asciidoctor/core/-/core-2.0.3.tgz#95630424eb2e346acc51dede1e6599159fbd5d44"
integrity sha512-iN0zV/tsL36nTltJyk5IdcRqDW34HHiwb9PrgSoTGDCIEML6C3HYnJOQi+jKq7A//Gt1nL1SHohaCHkyw3swEg==
dependencies:
asciidoctor-opal-runtime "0.3.0"
unxhr "1.0.1"
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.5.5":
version "7.5.5"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.5.5.tgz#bc0782f6d69f7b7d49531219699b988f669a8f9d"
@ -1400,22 +1385,6 @@ array-unique@^0.3.2:
resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
asciidoctor-opal-runtime@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/asciidoctor-opal-runtime/-/asciidoctor-opal-runtime-0.3.0.tgz#df327a870ddd3cd5eb0e162d64ed4dcdd3fe3fee"
integrity sha512-YapVwl2qbbs6sIe1dvAlMpBzQksFVTSa2HOduOKFNhZlE9bNmn+moDgGVvjWPbzMPo/g8gItyTHfWB2u7bQxag==
dependencies:
glob "7.1.3"
unxhr "1.0.1"
asciidoctor@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/asciidoctor/-/asciidoctor-2.0.3.tgz#350a41d051c5f99d1d8fa4abdc7a9c80da48f90a"
integrity sha512-6UB/oB0jRQOy6/DoFBH1nUzAAGt7Aa6snfXKZyvaxKxQHcuvFuJcvS2zVjAH2eURQidkBLy+J0VHBVjRWs8CiQ==
dependencies:
"@asciidoctor/cli" "2.0.1"
"@asciidoctor/core" "2.0.3"
asn1.js@^4.0.0:
version "4.10.1"
resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0"
@ -1505,14 +1474,6 @@ aws4@^1.8.0:
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==
axios@^0.19.0:
version "0.19.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.0.tgz#8e09bff3d9122e133f7b8101c8fbdd00ed3d2ab8"
integrity sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==
dependencies:
follow-redirects "1.5.10"
is-buffer "^2.0.2"
babel-eslint@^10.0.3:
version "10.0.3"
resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.0.3.tgz#81a2c669be0f205e19462fed2482d33e4687a88a"
@ -1680,7 +1641,7 @@ brace-expansion@^1.1.7:
balanced-match "^1.0.0"
concat-map "0.0.1"
brace@^0.11.0, brace@latest:
brace@^0.11.0:
version "0.11.1"
resolved "https://registry.yarnpkg.com/brace/-/brace-0.11.1.tgz#4896fcc9d544eef45f4bb7660db320d3b379fe58"
integrity sha1-SJb8ydVE7vRfS7dmDbMg07N5/lg=
@ -2648,13 +2609,6 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3:
dependencies:
ms "2.0.0"
debug@=3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
dependencies:
ms "2.0.0"
debug@^3.0.0, debug@^3.1.1, debug@^3.2.5, debug@^3.2.6:
version "3.2.6"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
@ -3563,13 +3517,6 @@ flush-write-stream@^1.0.0:
inherits "^2.0.3"
readable-stream "^2.3.6"
follow-redirects@1.5.10:
version "1.5.10"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a"
integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==
dependencies:
debug "=3.1.0"
follow-redirects@^1.0.0:
version "1.9.0"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.9.0.tgz#8d5bcdc65b7108fe1508649c79c12d732dcedb4f"
@ -3745,18 +3692,6 @@ glob-to-regexp@^0.3.0:
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab"
integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=
glob@7.1.3:
version "7.1.3"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1"
integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.0.4"
once "^1.3.0"
path-is-absolute "^1.0.0"
glob@^7.0.3, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4:
version "7.1.5"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.5.tgz#6714c69bee20f3c3e64c4dd905553e532b40cdc0"
@ -4347,11 +4282,6 @@ is-buffer@^1.1.5:
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
is-buffer@^2.0.2:
version "2.0.4"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.4.tgz#3e572f23c8411a5cfd9557c849e3665e0b290623"
integrity sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==
is-callable@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75"
@ -5660,7 +5590,7 @@ os-homedir@^1.0.0:
resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M=
os-locale@^3.0.0, os-locale@^3.1.0:
os-locale@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a"
integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==
@ -7823,11 +7753,6 @@ unset-value@^1.0.0:
has-value "^0.3.1"
isobject "^3.0.0"
unxhr@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/unxhr/-/unxhr-1.0.1.tgz#92200322d66c728993de771f9e01eeb21f41bc7b"
integrity sha512-MAhukhVHyaLGDjyDYhy8gVjWJyhTECCdNsLwlMoGFoNJ3o79fpQhtQuzmAE4IxCMDwraF4cW8ZjpAV0m9CRQbg==
upath@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894"
@ -8303,7 +8228,7 @@ yargs-parser@^11.1.1:
camelcase "^5.0.0"
decamelize "^1.2.0"
yargs-parser@^13.0.0, yargs-parser@^13.1.1:
yargs-parser@^13.1.1:
version "13.1.1"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.1.tgz#d26058532aa06d365fe091f6a1fc06b2f7e5eca0"
integrity sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==
@ -8329,23 +8254,6 @@ yargs@12.0.5:
y18n "^3.2.1 || ^4.0.0"
yargs-parser "^11.1.1"
yargs@13.2.2:
version "13.2.2"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.2.2.tgz#0c101f580ae95cea7f39d927e7770e3fdc97f993"
integrity sha512-WyEoxgyTD3w5XRpAQNYUB9ycVH/PQrToaTXdYXRdOXvEy1l19br+VJsc0vcO8PTGg5ro/l/GY7F/JMEBmI0BxA==
dependencies:
cliui "^4.0.0"
find-up "^3.0.0"
get-caller-file "^2.0.1"
os-locale "^3.1.0"
require-directory "^2.1.1"
require-main-filename "^2.0.0"
set-blocking "^2.0.0"
string-width "^3.0.0"
which-module "^2.0.0"
y18n "^4.0.0"
yargs-parser "^13.0.0"
yargs@^13.0.0:
version "13.3.0"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.0.tgz#4c657a55e07e5f2cf947f8a366567c04a0dedc83"

11
main.go
View File

@ -1,7 +1,6 @@
package main
import (
"code.chrissexton.org/cws/cabinet/entry"
"flag"
"os"
@ -17,9 +16,9 @@ import (
)
var (
dbPath = flag.String("db", "cabinet.db", "path to db")
dbPath = flag.String("db", "happy.db", "path to db")
httpAddr = flag.String("httpAddr", "0.0.0.0:8080", "http address")
salt = flag.String("salt", "c4b1n3t", "salt for IDs")
salt = flag.String("salt", "happy", "salt for IDs")
minHashLen = flag.Int("minHashLen", 4, "minimum ID hash size")
develop = flag.Bool("develop", false, "turn on develop mode")
)
@ -39,12 +38,6 @@ func main() {
Msg("could not connect to database")
}
tx := db.MustBegin()
if err := entry.PrepareTable(tx); err != nil {
log.Fatal().Err(err).Msg("could not create database")
}
tx.Commit()
s := web.New(*httpAddr, db, box)
s.Serve()
}

View File

@ -9,19 +9,13 @@
* Vue Frontend
** [ ] spend some time learning about TypeScript/Vue.js style
** Documents
*** [x] adoc editor widget
*** [ ] adoc editor widget
** Authentication
*** [ ] some kind of user auth
** Views
*** [ ] editor view
*** [ ] public index/search view
* Backend
** [?] save endpoint
*** [ ] need to generate a slug for entries
*** [ ] add authentication/authorization
*** [ ] convert document to adoc (give format?)
*** [x] test in frontend
*** [ ] check for unique tags
*** [ ] set some db fields not null
** [ ] save endpoint
** [ ] search endpoint
* CLI Frontend
* CLI Frontend

View File

@ -1,135 +0,0 @@
package web
import (
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/rs/zerolog/log"
"code.chrissexton.org/cws/cabinet/entry"
"github.com/gorilla/mux"
)
func (web *Web) editEntry(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
slug := vars["slug"]
dec := json.NewDecoder(r.Body)
newEntry := entry.New(web.db)
err := dec.Decode(&newEntry)
if err != nil {
w.WriteHeader(500)
fmt.Fprint(w, err)
return
}
oldEntry, err := entry.GetBySlug(web.db, slug)
if err != nil {
w.WriteHeader(500)
fmt.Fprint(w, err)
return
}
oldEntry.Content = newEntry.Content
oldEntry.Tags = newEntry.Tags
oldEntry.Updated = time.Now()
err = oldEntry.Update()
if err != nil {
w.WriteHeader(500)
fmt.Fprint(w, err)
return
}
resp, err := json.Marshal(oldEntry)
if err != nil {
w.WriteHeader(500)
fmt.Fprint(w, err)
return
}
log.Debug().Interface("oldEntry", oldEntry).Msg("Got a PUT")
w.Header().Set("content-type", "application/json")
fmt.Fprint(w, string(resp))
}
func (web *Web) newEntry(w http.ResponseWriter, r *http.Request) {
dec := json.NewDecoder(r.Body)
newEntry := entry.New(web.db)
err := dec.Decode(&newEntry)
if err != nil {
w.WriteHeader(500)
fmt.Fprint(w, err)
return
}
err = newEntry.Create()
if err != nil {
w.WriteHeader(500)
fmt.Fprint(w, err)
return
}
resp, err := json.Marshal(newEntry)
if err != nil {
w.WriteHeader(500)
fmt.Fprint(w, err)
return
}
w.Header().Set("content-type", "application/json")
fmt.Fprint(w, string(resp))
}
func (web *Web) allEntries(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
query := r.Form.Get("query")
entries, err := entry.Search(web.db, query)
if err != nil {
w.WriteHeader(500)
fmt.Fprint(w, err)
return
}
resp, err := json.Marshal(entries)
if err != nil {
w.WriteHeader(500)
fmt.Fprint(w, err)
return
}
w.Header().Set("content-type", "application/json")
fmt.Fprint(w, string(resp))
}
func (web *Web) getEntry(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
slug := vars["slug"]
entry, err := entry.GetBySlug(web.db, slug)
if err != nil {
w.WriteHeader(500)
fmt.Fprint(w, err)
return
}
resp, err := json.Marshal(entry)
if err != nil {
w.WriteHeader(500)
fmt.Fprint(w, err)
return
}
w.Header().Set("content-type", "application/json")
fmt.Fprint(w, string(resp))
}
func (web *Web) removeEntry(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
slug := vars["slug"]
err := entry.RemoveBySlug(web.db, slug)
if err != nil {
w.WriteHeader(500)
fmt.Fprint(w, err)
return
}
w.WriteHeader(200)
}

View File

@ -41,12 +41,7 @@ func New(addr string, db *db.Database, box *packr.Box) *Web {
func (web *Web) routeSetup() http.Handler {
r := mux.NewRouter()
api := r.PathPrefix("/v1/").Subrouter()
api.HandleFunc("/entries", web.allEntries).Methods(http.MethodGet)
api.HandleFunc("/entries", web.newEntry).Methods(http.MethodPost)
api.HandleFunc("/entries", web.removeEntry).Methods(http.MethodDelete)
api.HandleFunc("/entries/{slug}", web.editEntry).Methods(http.MethodPut)
api.HandleFunc("/entries/{slug}", web.getEntry).Methods(http.MethodGet)
//api := r.PathPrefix("/v1/").Subrouter()
r.PathPrefix("/").HandlerFunc(web.indexHandler("/index.html"))
loggedRouter := handlers.LoggingHandler(os.Stdout, r)
return loggedRouter

View File

@ -1,7 +0,0 @@
package web
import "net/http"
func search(w http.ResponseWriter, r *http.Request) {
}