git ssb

0+

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