Skip to content

Commit

Permalink
Add basic tags page
Browse files Browse the repository at this point in the history
  • Loading branch information
bcomnes committed Jun 22, 2022
1 parent e6e21ee commit 724af74
Show file tree
Hide file tree
Showing 10 changed files with 265 additions and 61 deletions.
12 changes: 12 additions & 0 deletions migrations/002.do.update-prune-tag.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
CREATE OR REPLACE FUNCTION prune_tags()
RETURNS TRIGGER AS $$
BEGIN
DELETE FROM tags
WHERE id IN (
SELECT id FROM tags
LEFT OUTER JOIN bookmarks_tags on tags.id = bookmarks_tags.tag_id
WHERE bookmarks_tags.tag_id is null
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
17 changes: 17 additions & 0 deletions migrations/002.undo.update-prune-tag.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
CREATE OR REPLACE FUNCTION prune_tags()
RETURNS TRIGGER AS $$
BEGIN
DELETE FROM tags
WHERE id IN (
SELECT id FROM tags
LEFT OUTER JOIN bookmarks_tags on tags.id = bookmarks_tags.tag_id
WHERE bookmarks_tags.tag_id is null
AND owner_id in (
SELECT owner_id
FROM tags
WHERE id = OLD.tag_id
)
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
105 changes: 66 additions & 39 deletions routes/api/bookmarks.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ export default async function bookmarkRoutes (fastify, opts) {
type: 'string',
format: 'uri'
},
tag: {
type: 'string', minLength: 1, maxLength: 255
},
sensitive: {
type: 'boolean',
default: false
Expand Down Expand Up @@ -89,7 +92,14 @@ export default async function bookmarkRoutes (fastify, opts) {
},
async function (request, reply) {
const id = request.user.id
let { before, after, per_page: perPage, url, sensitive } = request.query
let {
before,
after,
per_page: perPage,
url,
tag,
sensitive
} = request.query

let top = false
let bottom = false
Expand All @@ -103,10 +113,17 @@ export default async function bookmarkRoutes (fastify, opts) {
WITH page as (
SELECT id, url, title, created_at
FROM bookmarks
LEFT OUTER JOIN(
SELECT bt.bookmark_id as id, array_agg(t.name) as tag_array
FROM bookmarks_tags bt
JOIN tags t ON t.id = bt.tag_id
GROUP BY bt.bookmark_id
) t using (id)
WHERE owner_id = ${id}
AND created_at >= ${after}
${url ? SQL`AND url = ${url}` : SQL``}
${!sensitive ? SQL`AND sensitive = false` : SQL``}
${tag ? SQL`AND t.tag_array @> ARRAY[${tag}::citext]` : SQL``}
ORDER BY
created_at ASC, title ASC, url ASC
FETCH FIRST ${perPageAfterOffset} ROWS ONLY
Expand Down Expand Up @@ -143,10 +160,10 @@ export default async function bookmarkRoutes (fastify, opts) {
}

const query = SQL`
SELECT id, url, title, note, created_at, updated_at, toread, sensitive, starred, t.tag_array as tags
SELECT id, url, title, note, created_at, updated_at, toread, sensitive, starred, array_to_json(t.tag_array) as tags
FROM bookmarks
LEFT OUTER JOIN(
SELECT bt.bookmark_id as id, jsonb_agg(t.name) as tag_array
SELECT bt.bookmark_id as id, array_agg(t.name) as tag_array
FROM bookmarks_tags bt
JOIN tags t ON t.id = bt.tag_id
GROUP BY bt.bookmark_id
Expand All @@ -155,6 +172,7 @@ export default async function bookmarkRoutes (fastify, opts) {
${before ? SQL`AND created_at < ${before}` : SQL``}
${url ? SQL`AND url = ${url}` : SQL``}
${!sensitive ? SQL`AND sensitive = false` : SQL``}
${tag ? SQL`AND t.tag_array @> ARRAY[${tag}::citext]` : SQL``}
ORDER BY
created_at DESC, title DESC, url DESC
FETCH FIRST ${perPage} ROWS ONLY;
Expand Down Expand Up @@ -277,31 +295,31 @@ export default async function bookmarkRoutes (fastify, opts) {
}
)

fastify.get('/bookmarks/:id', {
preHandler: fastify.auth([fastify.verifyJWT]),
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string', format: 'uuid' }
},
required: ['id']
},
response: {
200: {
fastify.get(
'/bookmarks/:id', {
preHandler: fastify.auth([fastify.verifyJWT]),
schema: {
params: {
type: 'object',
properties: {
...fullBookmarkProps
id: { type: 'string', format: 'uuid' }
},
required: ['id']
},
response: {
200: {
type: 'object',
properties: {
...fullBookmarkProps
}
}
}
}
}
},
async function (request, reply) {
const userId = request.user.id
const { id: bookmarkId } = request.params
}, async function (request, reply) {
const userId = request.user.id
const { id: bookmarkId } = request.params

const query = SQL`
const query = SQL`
SELECT id, url, title, note, created_at, updated_at, toread, sensitive, starred, t.tag_array as tags
FROM bookmarks
LEFT OUTER JOIN(
Expand All @@ -315,18 +333,18 @@ export default async function bookmarkRoutes (fastify, opts) {
LIMIT 1;
`

const results = await fastify.pg.query(query)
const bookmark = results.rows[0]
if (!bookmark) {
reply.code(404)
const results = await fastify.pg.query(query)
const bookmark = results.rows[0]
if (!bookmark) {
reply.code(404)
return {
status: 'bookmark id not found'
}
}
return {
status: 'bookmark id not found'
...bookmark
}
}
return {
...bookmark
}
})
})

fastify.put('/bookmarks/:id', {
preHandler: fastify.auth([fastify.verifyJWT]),
Expand Down Expand Up @@ -374,8 +392,9 @@ export default async function bookmarkRoutes (fastify, opts) {
await client.query(query)
}

if (bookmark.tags?.length > 0) {
const createTags = SQL`
if (Array.isArray(bookmark.tags)) {
if (bookmark.tags.length > 0) {
const createTags = SQL`
INSERT INTO tags (name, owner_id)
VALUES
${SQL.glue(
Expand All @@ -388,9 +407,9 @@ export default async function bookmarkRoutes (fastify, opts) {
returning id, name, created_at, updated_at;
`

const tagsResults = await client.query(createTags)
const tagsResults = await client.query(createTags)

const applyTags = SQL`
const applyTags = SQL`
INSERT INTO bookmarks_tags (bookmark_id, tag_id)
VALUES
${SQL.glue(
Expand All @@ -401,15 +420,23 @@ export default async function bookmarkRoutes (fastify, opts) {
DO NOTHING;
`

await client.query(applyTags)
await client.query(applyTags)

const removeOldTags = SQL`
const removeOldTags = SQL`
DELETE FROM bookmarks_tags
WHERE bookmark_id = ${bookmarkId}
AND tag_id NOT IN (${SQL.glue(tagsResults.rows.map(tag => SQL`${tag.id}`), ', ')})
`

await client.query(removeOldTags)
await client.query(removeOldTags)
} else {
const removeAllTags = SQL`
DELETE FROM bookmarks_tags
WHERE bookmark_id = ${bookmarkId}
`

await client.query(removeAllTags)
}
}

return {
Expand Down
123 changes: 107 additions & 16 deletions routes/api/tags.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,123 @@
import SQL from '@nearform/sql'

export default async function tagsRoutes (fastify, opts) {
fastify.get(
'/tags',
{},
async (request, reply) => {
throw new Error('not implemented')
{
preHandler: fastify.auth([fastify.verifyJWT]),
schema: {},
response: {
200: {
type: 'object',
properties: {
data: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string' },
count: { type: 'integer' }
}
}
}
}
}
}
},
async function (request, reply) {
const userId = request.user.id

const query = SQL`
select name, created_at, count(bookmarks_tags.tag_id) as count
from tags
left outer join bookmarks_tags on (tags.id = bookmarks_tags.tag_id)
where owner_id = ${userId}
group by (name, created_at)
order by created_at desc;
`

const results = await fastify.pg.query(query)

return {
data: results.rows
}
}
)

fastify.get(
'/tags/rename',
{},
async (request, reply) => {
throw new Error('not implemented')
fastify.delete(
'/tags/:name',
{
preHandler: fastify.auth([fastify.verifyJWT]),
schema: {
params: {
type: 'object',
properties: {
name: { type: 'string' }
},
required: ['name']
}
}
},
async function (request, reply) {
const userId = request.user.id
const tagName = request.params.name

const query = SQL`
delete from tags
where name = ${tagName}
AND owner_id =${userId};
`

await fastify.pg.query(query)

reply.status = 202
return {
status: 'ok'
}
}
)

fastify.get(
'/tags/merge',
{},
async (request, reply) => {
fastify.post(
'/tags/rename',
{
preHandler: fastify.auth([fastify.verifyJWT]),
schema: {
body: {
type: 'object',
properties: {
old: { type: 'string', minLength: 1, maxLength: 255 },
new: { type: 'string', minLength: 1, maxLength: 255 }
},
required: ['name']
}
}
},
async function (request, reply) {
throw new Error('not implemented')
}
)

fastify.get(
'/tags/delete',
{},
async (request, reply) => {
fastify.post(
'/tags/merge',
{
preHandler: fastify.auth([fastify.verifyJWT]),
schema: {
body: {
type: 'object',
properties: {
source: {
type: ['array', 'null'],
items: {
type: 'string', minLength: 1, maxLength: 255
}
},
target: { type: 'string', minLength: 1, maxLength: 255 }
},
required: ['name']
}
}
},
async function (request, reply) {
throw new Error('not implemented')
}
)
Expand Down
2 changes: 1 addition & 1 deletion web/bookmarks/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export const page = Component(() => {
${bookmarksError ? html`<div>${bookmarksError.message}</div>` : null}
${Array.isArray(bookmarks)
? bookmarks.map(b => html.for(b, b.id)`${bookmarkList({ bookmark: b, reload })}`)
: null}
: null}
<div>
${before ? html`<a href=${'./?' + new URLSearchParams(`before=${before.valueOf()}`)}>earlier</a>` : null}
${after ? html`<a href=${'./?' + new URLSearchParams(`after=${after.valueOf()}`)}>later</span>` : null}
Expand Down
7 changes: 6 additions & 1 deletion web/components/bookmark/bookmark-view.css
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,12 @@
color: var(--accent-foreground);
}

.bc-tags-display {
display: inline-flex;
gap: 5px;
flex-wrap: wrap;
}

.bc-tags-display a {
margin-right: 5px;
color: var(--bc-tags-color);
}
2 changes: 1 addition & 1 deletion web/components/bookmark/bookmark-view.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export const bookmarkView = Component(({
? html`
<div class="bc-tags-display">
🏷
${b.tags.map(tag => html`<a href=${`/tags/view/?tag=${tag}`}>${tag}</a> `)}
${b.tags.map(tag => html` <a href=${`/bookmarks/?tag=${tag}`}>${tag}</a> `)}
</div>`
: null
}
Expand Down

0 comments on commit 724af74

Please sign in to comment.