git ssb

0+

Kira / %V53yIAO6ZNGv1Lx9tCP…



forked from cel / ssb-issues

Commit 230fe608ef1516dbdc57be322b43aa7d33813625

Initial commit

Charles Lehner committed on 3/25/2016, 11:51:02 PM

Files changed

README.mdadded
index.jsadded
lib/schemas.jsadded
package.jsonadded
test.jsadded
README.mdView
@@ -1,0 +1,174 @@
1+# ssb-issues
2+
3+Issue tracking built on secure-scuttlebutt
4+
5+## Schema
6+
7+#### type: issue
8+
9+An issue. Represents something that should be fixed.
10+
11+```js
12+{
13+ type: 'issue',
14+ project: Link?,
15+ title: string?,
16+ text: string?
17+}
18+```
19+
20+#### type: issue-edit
21+
22+An edit to an issue. Only considered valid if its author is the author of the
23+issue (`issue.author`) or of the project of the issue (`issue.projectAuthor`).
24+
25+```js
26+{
27+ type: 'issue-edit',
28+ issue: IssueRef,
29+ open: boolean?,
30+ title: string?
31+}
32+```
33+
34+## API
35+
36+```js
37+var Issues = require('ssb-issues')
38+var issues = Issues.init(sbot)
39+```
40+
41+#### get: async
42+
43+Get an issue by its id
44+
45+```js
46+issues.get(issueId, cb)
47+```
48+
49+The resulting issue object is as follows:
50+
51+```js
52+{
53+ id: MsgRef,
54+ author: FeedRef,
55+ project: Ref?,
56+ projectAuthor: FeedRef?,
57+ created_at: number,
58+ updated_at: number,
59+ open: boolean
60+}
61+```
62+
63+- `id`: id of the issue
64+- `author`: author of the issue
65+- `created_at` (timestamp): when the issue was created
66+- `updated_at` (timestamp): when the issue was last updated
67+- `title`: title of the issue
68+- `open`: whether the issue is open (true) or closed (false)
69+- `project`: the project that the issue is for
70+- `projectAuthor`: the author of the project
71+
72+#### createFeedStream: source
73+
74+Get a stream of issues
75+
76+```js
77+issues.createFeedStream({ project:, open:, live:, gt:, gte:, lt:, lte:, reverse:, limit:, }, cb)
78+```
79+
80+ - `project` (Ref): get only issues for the given target
81+ - `open` (boolean): get only open or closed issues
82+ - `author` (FeedRef): get only issues from the given feed
83+ - `live` (boolean, default: `false`): Keep the stream open and emit new messages as they are received.
84+ - `gt` (greater than), `gte` (greater than or equal): maximum `[timestamp, id]`
85+ - `lt` (less than), `lte` (less than or equal): minimum `[timestamp, id]`
86+ - `reverse` (boolean, default: `false`): reverse the order of results
87+
88+#### new: async
89+
90+Create a new issue
91+
92+```js
93+issues.new({ project:, title:, text: }, cb)
94+```
95+
96+- `project` (Ref): id of an ssb object representing the target of the issue
97+- `title` (string): title of the issue
98+- `text` (string): text describing the issue
99+
100+#### close: async
101+
102+```js
103+issues.close(id, cb)
104+```
105+
106+Mark an issue as closed.
107+
108+`id` (MsgRef): id of the issue to reopen
109+
110+#### reopen: async
111+
112+```js
113+issues.reopen(id, cb)`
114+```
115+
116+Mark an issue as open.
117+
118+`id` (MsgRef): id of the issue to reopen
119+`text` (string): text to accompany the open action
120+
121+#### edit: async
122+
123+```js
124+issues.edit(id, opts, cb)`
125+```
126+
127+Edit an issue.
128+
129+`id` (MsgRef): id of the issue to reopen
130+`opts.open` (boolean): set open/closed status
131+`opts.title` (string): set title
132+
133+### schemas
134+
135+```js
136+var issueSchemas = Issues.schemas
137+```
138+
139+#### `issueSchemas.new(project, title, text)`
140+
141+Create a new issue.
142+
143+- `project` (Ref): id of project to associate the issue with
144+- `title` (string): title to give the issue
145+- `text` (string): text body for the issue
146+
147+#### `issueSchemas.edit(id, opts)`
148+
149+Edit an issue.
150+
151+- `opts.open` (boolean): open or close the issue
152+- `opts.title` (string): set the title of the issue
153+
154+#### `issueSchemas.close(id)`
155+
156+Close an issue.
157+
158+- `id` (MsgRef): id of an issue to mark as closed
159+
160+#### `issueSchemas.reopen(id)`
161+
162+Reopen an issue.
163+
164+- `id` (MsgRef): id of an issue to mark as open
165+
166+## License
167+
168+Copyright (c) 2016 Charles Lehner
169+
170+Usage of the works is permitted provided that this instrument is
171+retained with the works, so that any entity that uses the works is
172+notified of this instrument.
173+
174+DISCLAIMER: THE WORKS ARE WITHOUT WARRANTY.
index.jsView
@@ -1,0 +1,182 @@
1+var pull = require('pull-stream')
2+var paramap = require('pull-paramap')
3+var asyncMemo = require('asyncmemo')
4+var issueSchemas = require('./lib/schemas')
5+
6+function Cache(fn, ssb) {
7+ return asyncMemo(fn)
8+ return function (key, cb) { ac.get(key, cb) }
9+}
10+
11+function isUpdateValid(issue, msg) {
12+ return msg.value.author == issue.author
13+ && msg.value.author == issue.projectAuthor
14+}
15+
16+exports.name = 'issues'
17+
18+exports.manifest = {
19+ get: 'async',
20+ createFeedStream: 'source',
21+ new: 'async',
22+ edit: 'async',
23+ close: 'async',
24+ reopen: 'async'
25+}
26+
27+exports.schemas = issueSchemas
28+
29+exports.init = function (ssb) {
30+
31+ var ssbGet = asyncMemo(ssb.get)
32+
33+ var getIssue = asyncMemo(function (id, cb) {
34+ var issue = {}
35+ var issueMsg
36+
37+ if (id && id.value && id.key) {
38+ var msg = id
39+ id = id.key
40+ gotIssueMsg(null, msg)
41+ } else {
42+ ssbGet(id, gotIssueMsg)
43+ }
44+
45+ function gotIssueMsg(err, msg) {
46+ if (err) return cb(err)
47+ issueMsg = msg
48+ issue.id = msg.key
49+ issue.author = msg.value.author
50+ var c = msg.value.content
51+ issue.project = c.project
52+ issue.text = c.text
53+ issue.created_at = issue.updated_at = msg.value.timestamp
54+ if (c.project)
55+ ssbGet(c.project, gotProjectMsg)
56+ else
57+ getLinks()
58+ }
59+
60+ function gotProjectMsg(err, msg) {
61+ if (err) return cb(err)
62+ issue.projectAuthor = msg.author
63+ getLinks()
64+ }
65+
66+ function getLinks() {
67+ var now = Date.now()
68+ // compute the result from the past data
69+ pull(
70+ ssb.links({dest: id, lt: now, reverse: true}),
71+ pull.drain(onOldMsg, onOldEnd)
72+ )
73+ // keep the results up-to-date in the future
74+ pull(
75+ ssb.links({dest: id, gte: now, values: true, live: true}),
76+ pull.drain(onNewMsg, onNewEnd)
77+ )
78+ }
79+
80+ function onOldMsg(msg) {
81+ if (!isUpdateValid(issue, msg))
82+ return
83+ var c = msg.value.content
84+ if (c.open != null && issue.open == null)
85+ issue.open = c.open
86+ if (c.title != null && issue.title == null)
87+ issue.title = c.title
88+ if (msg.value.timestamp > issue.updated_at)
89+ issue.updated_at = msg.value.timestamp
90+ checkReady()
91+ }
92+
93+ function onNewMsg(msg) {
94+ if (!isUpdateValid(issue, msg))
95+ return
96+ var c = msg.value.content
97+ if (c.open != null)
98+ issue.open = c.open
99+ if (c.title != null)
100+ issue.title = c.title
101+ if (msg.value.timestamp > issue.updated_at)
102+ issue.updated_at = msg.value.timestamp
103+ }
104+
105+ function checkReady() {
106+ // call back once all the issue properties are set
107+ if (issue.open != null && issue.title != null) {
108+ var _cb = cb
109+ delete cb
110+ _cb(null, issue)
111+ }
112+ }
113+
114+ function onOldEnd(err) {
115+ if (err) {
116+ if (cb) cb(err)
117+ else console.error(err)
118+ return
119+ }
120+ // process the root message last
121+ onOldMsg(issueMsg)
122+ // if callback hasn't been called yet, the issue is missing a field
123+ if (cb) {
124+ if (issue.open == null)
125+ issue.open = true
126+ if (issue.title == null)
127+ issue.title = issue.id
128+ checkReady()
129+ }
130+ }
131+
132+ function onNewEnd(err) {
133+ if (err) {
134+ if (cb) cb(err)
135+ else console.error(err)
136+ }
137+ }
138+ })
139+
140+ function createFeedStream(opts) {
141+ opts.type = 'issue'
142+ delete opts.limit
143+ return pull(
144+ // TODO: use links2 for this
145+ ssb.messagesByType(opts),
146+ pull.filter(function (msg) {
147+ return (!opts.project || opts.project == msg.value.content.project)
148+ && (!opts.author || opts.author == msg.value.author)
149+ }),
150+ paramap(getIssue, 8)
151+ )
152+ }
153+
154+ function editIssue(id, opts, cb) {
155+ ssb.publish(issueSchemas.edit(id, opts), cb)
156+ }
157+
158+ function closeIssue(id, cb) {
159+ ssb.publish(issueSchemas.close(id), cb)
160+ }
161+
162+ function reopenIssue(id, cb) {
163+ ssb.publish(issueSchemas.reopen(id), cb)
164+ }
165+
166+ function newIssue(opts, cb) {
167+ var msg = issueSchemas.new(opts.project, opts.title, opts.text)
168+ ssb.publish(msg, function (err, msg) {
169+ if (err) return cb(err)
170+ getIssue(msg, cb)
171+ })
172+ }
173+
174+ return {
175+ get: getIssue,
176+ createFeedStream: createFeedStream,
177+ new: newIssue,
178+ edit: editIssue,
179+ close: closeIssue,
180+ reopen: reopenIssue
181+ }
182+}
lib/schemas.jsView
@@ -1,0 +1,43 @@
1+var ssbRef = require('ssb-ref')
2+var mlib = require('ssb-msgs')
3+
4+exports.new = function (project, title, text) {
5+ if (!ssbRef.isLink(project))
6+ throw new Error('invalid project id')
7+ var msg = { type: 'issue', project: project }
8+ if (title) {
9+ if (typeof title === 'string')
10+ msg.title = title
11+ else
12+ throw new Error('invalid issue title')
13+ }
14+ if (text) {
15+ if (typeof text === 'string')
16+ msg.text = text
17+ else
18+ throw new Error('invalid issue text')
19+ }
20+ return msg
21+}
22+
23+exports.edit = function (id, opts) {
24+ if (!ssbRef.isMsg(id))
25+ throw new Error('invalid issue id')
26+ var msg = {
27+ type: 'issue-edit',
28+ issue: id
29+ }
30+ if (opts.open != null)
31+ msg.open = opts.open
32+ if (opts.title != null)
33+ msg.title = opts.title
34+ return msg
35+}
36+
37+exports.close = function (id) {
38+ return exports.edit(id, {open: false})
39+}
40+
41+exports.reopen = function (id) {
42+ return exports.edit(id, {open: true})
43+}
package.jsonView
@@ -1,0 +1,27 @@
1+{
2+ "name": "ssb-issues",
3+ "version": "0.0.0",
4+ "description": "Issue tracking built on secure-scuttlebutt",
5+ "main": "index.js",
6+ "author": "Charles Lehner (http://celehner.com/)",
7+ "license": "Fair",
8+ "homepage": "http://git-ssb.celehner.com/",
9+ "scripts": {
10+ "test": "node test"
11+ },
12+ "dependencies": {
13+ "asyncmemo": "^0.1.0",
14+ "pull-cat": "^1.1.8",
15+ "pull-paramap": "^1.1.2",
16+ "pull-stream": "^3.2.0",
17+ "ssb-client": "^3.0.1",
18+ "ssb-links": "^1.0.1",
19+ "ssb-msgs": "^5.2.0",
20+ "ssb-ref": "^2.3.0"
21+ },
22+ "devDependencies": {
23+ "scuttlebot": "^7.6.6",
24+ "ssb-keys": "^5.0.0",
25+ "tape": "^4.5.1"
26+ }
27+}
test.jsView
@@ -1,0 +1,99 @@
1+var test = require('tape')
2+var Issues = require('.')
3+var ssbKeys = require('ssb-keys')
4+var pull = require('pull-stream')
5+
6+function awaitMsg(sbot, msg, cb) {
7+ sbot.createUserStream({
8+ id: msg.value.author,
9+ gte: msg.value.sequence,
10+ live: true,
11+ limit: 1
12+ })(null, cb)
13+}
14+
15+var createSbot = require('scuttlebot')
16+ .use(require('scuttlebot/plugins/master'))
17+ .use(require('scuttlebot/plugins/blobs'))
18+
19+var sbot = createSbot({
20+ temp: 'test-ssb-issues', timeout: 200,
21+ allowPrivate: true,
22+ keys: ssbKeys.generate()
23+})
24+
25+var issues = Issues.init(sbot)
26+
27+test.onFinish(function () {
28+ sbot.close(true)
29+})
30+
31+var projectId
32+
33+test('create project', function (t) {
34+ sbot.publish({type: 'foo'}, function (err, msg) {
35+ t.error(err, 'publish')
36+ projectId = msg.key
37+ t.end()
38+ })
39+})
40+
41+var issue1
42+
43+test('create an issue', function (t) {
44+ var title = 'Test Title'
45+ issues.new({
46+ project: projectId,
47+ title: title,
48+ text: 'Test Text'
49+ }, function (err, issue) {
50+ t.error(err, 'new issue')
51+ t.ok(issue.id, 'id')
52+ t.equals(issue.project, projectId, 'project')
53+ t.equals(issue.author, sbot.id, 'author')
54+ t.equals(issue.title, title, 'title')
55+ t.equals(issue.open, true, 'open')
56+ t.equals(~~(issue.created_at/1e5), ~~(Date.now()/1e5), 'created_at')
57+ issue1 = issue
58+ t.end()
59+ })
60+})
61+
62+test('update the issue', function (t) {
63+ var title = 'New Title'
64+ issues.edit(issue1.id, {title: title}, function (err, msg) {
65+ t.error(err, 'edit')
66+ t.ok(msg, 'msg')
67+ awaitMsg(sbot, msg, function (err) {
68+ t.error(err, 'await')
69+ t.equals(issue1.title, title, 'new title')
70+ t.notEquals(issue1.updated_at, issue1.created_at, 'updated_at is new')
71+ t.ok(Date.now() - issue1.updated_at < 60e3, 'updated_at is recent')
72+ t.end()
73+ })
74+ })
75+})
76+
77+test('close the issue', function (t) {
78+ issues.close(issue1.id, function (err, msg) {
79+ t.error(err, 'close')
80+ t.ok(msg, 'msg')
81+ awaitMsg(sbot, msg, function (err) {
82+ t.error(err, 'await')
83+ t.equals(issue1.open, false, 'closed')
84+ t.end()
85+ })
86+ })
87+})
88+
89+test('reopen the issue', function (t) {
90+ issues.reopen(issue1.id, function (err, msg) {
91+ t.error(err, 'reopen')
92+ t.ok(msg, 'msg')
93+ awaitMsg(sbot, msg, function (err) {
94+ t.error(err, 'await')
95+ t.equals(issue1.open, true, 'open')
96+ t.end()
97+ })
98+ })
99+})

Built with git-ssb-web