Add go links and minor refactoring

This commit is contained in:
Garrett Mills 2022-04-05 10:47:15 -05:00
parent 22c2b9f665
commit 3142d0a4be
15 changed files with 238 additions and 65 deletions

View File

@ -8,7 +8,6 @@ COPY lib/ /app
RUN rm -f /app/.env RUN rm -f /app/.env
COPY .env.docker /app/.env
COPY package.json /app COPY package.json /app
COPY pnpm-lock.yaml /app COPY pnpm-lock.yaml /app

12
ex
View File

@ -1,4 +1,4 @@
#!/bin/bash -e #!/bin/bash
ENV_NODE="$(which node)" ENV_NODE="$(which node)"
ENV_PNPM="$(which pnpm)" ENV_PNPM="$(which pnpm)"
@ -134,7 +134,13 @@ if [ ! -d "./node_modules" ]; then
printf "\033[32m✓\033[39m Looks like you're all set up! Run this command again to access the Extollo CLI.\n" printf "\033[32m✓\033[39m Looks like you're all set up! Run this command again to access the Extollo CLI.\n"
else else
start_spinner "Building your app..." start_spinner "Building your app..."
"$ENV_PNPM" run build > /dev/null BUILD_OUTPUT="$($ENV_PNPM run build 2>&1)"
stop_spinner 0 BUILD_EC=$?
stop_spinner $BUILD_EC
if [ $BUILD_EC -ne 0 ]; then
printf "\033[31m✘\033[39m Uh, oh! Looks like your application failed to build. (exit: $BUILD_EC)"
echo "$BUILD_OUTPUT"
exit $BUILD_EC
fi
"$ENV_NODE" --experimental-repl-await ./lib/cli.js $@ "$ENV_NODE" --experimental-repl-await ./lib/cli.js $@
fi fi

View File

@ -8,8 +8,9 @@
"lib": "lib" "lib": "lib"
}, },
"dependencies": { "dependencies": {
"@extollo/lib": "^0.9.21", "@extollo/lib": "^0.9.28",
"copyfiles": "^2.4.1", "copyfiles": "^2.4.1",
"feed": "^4.2.2",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"ts-expose-internals": "^4.5.4", "ts-expose-internals": "^4.5.4",
"ts-patch": "^2.0.1", "ts-patch": "^2.0.1",

View File

@ -2,8 +2,9 @@ lockfileVersion: 5.3
specifiers: specifiers:
'@extollo/cc': ^0.6.0 '@extollo/cc': ^0.6.0
'@extollo/lib': ^0.9.21 '@extollo/lib': ^0.9.28
copyfiles: ^2.4.1 copyfiles: ^2.4.1
feed: ^4.2.2
rimraf: ^3.0.2 rimraf: ^3.0.2
ts-expose-internals: ^4.5.4 ts-expose-internals: ^4.5.4
ts-patch: ^2.0.1 ts-patch: ^2.0.1
@ -12,8 +13,9 @@ specifiers:
zod: ^3.11.6 zod: ^3.11.6
dependencies: dependencies:
'@extollo/lib': 0.9.21 '@extollo/lib': 0.9.28
copyfiles: 2.4.1 copyfiles: 2.4.1
feed: 4.2.2
rimraf: 3.0.2 rimraf: 3.0.2
ts-expose-internals: 4.5.4 ts-expose-internals: 4.5.4
ts-patch: 2.0.1_typescript@4.3.2 ts-patch: 2.0.1_typescript@4.3.2
@ -110,8 +112,8 @@ packages:
- supports-color - supports-color
dev: true dev: true
/@extollo/lib/0.9.21: /@extollo/lib/0.9.28:
resolution: {integrity: sha512-JKgUQWS9/lEho9ardayvZO8pkE4ZoPs4xuo6KAQDqp7wq4PjGjrGBKeHX7ViHeaFm/XelskaG9eu9Ox9RvirFQ==} resolution: {integrity: sha512-lMI2FWOTKbRsqJrOAvZofzfz0DaNwWecYsDX98XkNbktaUjhaGc7xr1OVxscxUC/Edapyj/p02MLdsUoIsARxA==}
dependencies: dependencies:
'@atao60/fse-cli': 0.1.7 '@atao60/fse-cli': 0.1.7
'@extollo/ui': 0.1.0_@types+node@14.18.12 '@extollo/ui': 0.1.0_@types+node@14.18.12
@ -987,6 +989,13 @@ packages:
dependencies: dependencies:
reusify: 1.0.4 reusify: 1.0.4
/feed/4.2.2:
resolution: {integrity: sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==}
engines: {node: '>=0.4.0'}
dependencies:
xml-js: 1.6.11
dev: false
/fetch-blob/3.1.5: /fetch-blob/3.1.5:
resolution: {integrity: sha512-N64ZpKqoLejlrwkIAnb9iLSA3Vx/kjgzpcDhygcqJ2KKjky8nCgUQ+dzXtbrLaWZGZNmNfQTsiQ0weZ1svglHg==} resolution: {integrity: sha512-N64ZpKqoLejlrwkIAnb9iLSA3Vx/kjgzpcDhygcqJ2KKjky8nCgUQ+dzXtbrLaWZGZNmNfQTsiQ0weZ1svglHg==}
engines: {node: ^12.20 || >= 14.13} engines: {node: ^12.20 || >= 14.13}
@ -2001,6 +2010,10 @@ packages:
/safer-buffer/2.1.2: /safer-buffer/2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
/sax/1.2.4:
resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==}
dev: false
/semver/5.7.1: /semver/5.7.1:
resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==} resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==}
hasBin: true hasBin: true
@ -2577,6 +2590,13 @@ packages:
/wrappy/1.0.2: /wrappy/1.0.2:
resolution: {integrity: sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=} resolution: {integrity: sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=}
/xml-js/1.6.11:
resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==}
hasBin: true
dependencies:
sax: 1.2.4
dev: false
/xtend/4.0.2: /xtend/4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'} engines: {node: '>=0.4'}

View File

@ -0,0 +1,8 @@
import {env, RedisOptions} from '@extollo/lib'
export default {
connection: {
port: env('REDIS_PORT', 6379),
host: env('REDIS_HOST', '127.0.0.1'),
} as RedisOptions
}

View File

@ -0,0 +1,16 @@
import {Controller, view, Injectable, Collection} from '@extollo/lib'
import {FeedPost} from '../../models/FeedPost.model'
/**
* Feed Controller
* ------------------------------------
* Backend for routes related to my post feed.
*/
@Injectable()
export class Feed extends Controller {
public async feed(feedPosts: Collection<FeedPost>) {
return view('feed', {
feedPosts: feedPosts.toArray(),
})
}
}

View File

@ -0,0 +1,22 @@
import {Controller, view, Inject, Injectable, HTTPStatus, http, redirect} from '@extollo/lib'
import {GoLink} from '../../models/GoLink.model'
/**
* GoLinks Controller
*/
@Injectable()
export class GoLinks extends Controller {
async launch() {
const short = this.request.safe('short').string()
const link = await GoLink.query<GoLink>()
.where('active', '=', true)
.where('short', '=', short)
.first()
if ( !link ) {
return http(HTTPStatus.http404, 'Invalid or expired link.')
}
return redirect(link.url)
}
}

View File

@ -13,9 +13,8 @@ export class Home extends Controller {
@Inject() @Inject()
protected readonly routing!: Routing protected readonly routing!: Routing
public async welcome() { public async welcome(feedPosts: Collection<FeedPost>) {
const workItems = await this.getWorkItems() const workItems = await this.getWorkItems()
const feedPosts = await this.getFeedPosts()
return view('welcome', { return view('welcome', {
feedPosts: feedPosts.toArray(), feedPosts: feedPosts.toArray(),
@ -30,13 +29,6 @@ export class Home extends Controller {
}) })
} }
public async feed() {
const feedPosts = await this.getFeedPosts(true)
return view('feed', {
feedPosts: feedPosts.toArray(),
})
}
public technical() { public technical() {
const isOptOut = this.request.cookies.has(this.config.get('app.analytics.optOutCookie')) const isOptOut = this.request.cookies.has(this.config.get('app.analytics.optOutCookie'))
@ -73,21 +65,6 @@ export class Home extends Controller {
}) })
} }
protected async getFeedPosts(all = false): Promise<Collection<FeedPost>> {
const query = FeedPost.query<FeedPost>()
.orderByDescending('posted_at')
if ( !all ) {
query.limit(6)
}
if ( !this.security.hasUser() ) {
query.where('visible', '=', true)
}
return query.get().collect()
}
protected async getWorkItems(): Promise<Collection<WorkItem>> { protected async getWorkItems(): Promise<Collection<WorkItem>> {
const query = WorkItem.query<WorkItem>() const query = WorkItem.query<WorkItem>()
.orderByDescending('start_date') .orderByDescending('start_date')

View File

@ -4,23 +4,14 @@ import {Snippet} from '../../models/Snippet.model'
/** /**
* Snippets Controller * Snippets Controller
* ------------------------------------ * ------------------------------------
* Put some description here. * Backend for routes that deal with code snippets.
*/ */
@Injectable() @Injectable()
export class Snippets extends Controller { export class Snippets extends Controller {
@Inject() @Inject()
protected readonly logging!: Logging protected readonly logging!: Logging
public async viewSnippet() { public async viewSnippet(snippet: Snippet) {
const slug = this.request.safe('slug').string()
const snippet = await Snippet.query<Snippet>()
.where('slug', '=', slug)
.first()
if ( !snippet ) {
return http(HTTPStatus.http404, 'Snippet URL is invalid.')
}
const needsAccessKey = snippet?.accessKey && snippet.accessKey !== this.request.input('accessKey') const needsAccessKey = snippet?.accessKey && snippet.accessKey !== this.request.input('accessKey')
const needsConfirm = snippet?.singleAccessOnly && !this.request.input('confirmSingleAccess') const needsConfirm = snippet?.singleAccessOnly && !this.request.input('confirmSingleAccess')

View File

@ -0,0 +1,28 @@
import {Injectable, ParameterMiddleware, Collection, SecurityContext, Either, ResponseObject, right, Inject} from '@extollo/lib'
import {FeedPost} from '../../../models/FeedPost.model'
/**
* LoadFeedPosts Middleware
* --------------------------------------------
* Load feed posts visible to the user as a handler parameter.
*/
@Injectable()
export class LoadFeedPosts extends ParameterMiddleware<Collection<FeedPost>, [] | [{ all: boolean }]> {
@Inject()
protected readonly security!: SecurityContext
async handle({ all = false } = {}): Promise<Either<ResponseObject, Collection<FeedPost>>> {
const query = FeedPost.query<FeedPost>()
.orderByDescending('posted_at')
if ( !all ) {
query.limit(6)
}
if ( !this.security.hasUser() ) {
query.where('visible', '=', true)
}
return right(await query.get().collect())
}
}

View File

@ -0,0 +1,40 @@
import {
Either,
http,
HTTPStatus,
Inject,
Injectable,
left,
ParameterMiddleware,
ResponseObject, right,
SecurityContext,
} from '@extollo/lib'
import {Snippet} from '../../../models/Snippet.model'
/**
* LoadSnippet Middleware
* --------------------------------------------
* Look up the Snippet instance from the request parameters.
*/
@Injectable()
export class LoadSnippet extends ParameterMiddleware<Snippet> {
@Inject()
protected readonly security!: SecurityContext
async handle(): Promise<Either<ResponseObject, Snippet>> {
const slug = String(this.request.input('slug') || '')
if ( !slug ) {
return left(http(HTTPStatus.http404))
}
const snippet = await Snippet.query<Snippet>()
.where('slug', '=', slug)
.first()
if ( !snippet || snippet.usersOnly && !this.security.hasUser() ) {
return left(http(HTTPStatus.http404))
}
return right(snippet)
}
}

View File

@ -2,10 +2,15 @@ import {Route, SessionAuthMiddleware} from '@extollo/lib'
import {Home} from '../controllers/Home.controller' import {Home} from '../controllers/Home.controller'
import {PageView} from '../middlewares/PageView.middleware' import {PageView} from '../middlewares/PageView.middleware'
import {Snippets} from '../controllers/Snippets.controller' import {Snippets} from '../controllers/Snippets.controller'
import {GoLinks} from '../controllers/GoLinks.controller'
import {Feed} from '../controllers/Feed.controller'
import {LoadSnippet} from '../middlewares/parameters/LoadSnippet.middleware'
import {LoadFeedPosts} from '../middlewares/parameters/LoadFeedPosts.middleware'
Route Route
.group('/', () => { .group('/', () => {
Route.get('/') Route.get('/')
.parameterMiddleware(LoadFeedPosts)
.calls<Home>(Home, home => home.welcome) .calls<Home>(Home, home => home.welcome)
.alias('home') .alias('home')
@ -21,16 +26,16 @@ Route
.alias('opt-out') .alias('opt-out')
Route.get('/feed') Route.get('/feed')
.calls<Home>(Home, home => home.feed) .parameterMiddleware(LoadFeedPosts, {all: true})
.calls<Feed>(Feed, feed => feed.feed)
.alias('feed') .alias('feed')
Route.get('/snippet/:slug') Route.get('/snippet/:slug')
.parameterMiddleware(LoadSnippet)
.calls<Snippets>(Snippets, snippets => snippets.viewSnippet) .calls<Snippets>(Snippets, snippets => snippets.viewSnippet)
Route.get('/test') Route.any('/go/:short')
.handledBy(() => { .calls<GoLinks>(GoLinks, go => go.launch)
return 'Hello, World!'
})
}) })
.pre(SessionAuthMiddleware) .pre(SessionAuthMiddleware)
.pre(PageView) .pre(PageView)

View File

@ -0,0 +1,49 @@
import {DatabaseService, FieldType, Inject, Injectable, Migration} from '@extollo/lib'
/**
* CreateGolinksTableMigration
* ----------------------------------
* Create Table to store URL redirections
*/
@Injectable()
export default class CreateGolinksTableMigration extends Migration {
@Inject()
protected readonly db!: DatabaseService
/**
* Apply the migration.
*/
async up(): Promise<void> {
const schema = this.db.get().schema()
const table = await schema.table('go_links')
table.primaryKey('go_link_id').required()
table.column('active')
.type(FieldType.bool)
.default(true)
table.column('short')
.type(FieldType.varchar)
.required()
.unique()
table.column('url')
.type(FieldType.text)
.required()
await schema.commit(table)
}
/**
* Undo the migration.
*/
async down(): Promise<void> {
const schema = this.db.get().schema()
const table = await schema.table('go_links')
table.dropIfExists()
await schema.commit(table)
}
}

View File

@ -0,0 +1,24 @@
import {Field, FieldType, Injectable, Model} from '@extollo/lib'
/**
* GoLink Model
* -----------------------------------
* A shortened URL.
*/
@Injectable()
export class GoLink extends Model<GoLink> {
protected static table = 'go_links'
protected static key = 'go_link_id'
@Field(FieldType.serial, 'go_link_id')
protected id!: number
@Field(FieldType.bool)
public active = true
@Field(FieldType.varchar)
public short!: string
@Field(FieldType.text)
public url!: string
}

View File

@ -64,19 +64,6 @@ block content
p.text !{item.body} p.text !{item.body}
.bottom .bottom
.stamp <a href="#{named('feed')}##{item.feedPostId}" class="feed-edit-button">permalink</a>&nbsp;&nbsp;|&nbsp;&nbsp;#{item.postedAt.toLocaleString()} .stamp <a href="#{named('feed')}##{item.feedPostId}" class="feed-edit-button">permalink</a>&nbsp;&nbsp;|&nbsp;&nbsp;#{item.postedAt.toLocaleString()}
// each item in feed_items .row.mt-4
// .feed-item .col-12.text-center
// div.feed-category(id='feedPostTag_' + item.id) a.button(href="/feed") view all
// .tag #{item.tag}
// span
// if item.draft
// p.text (draft)
// p.text !{item.text}
// .bottom
// if user && can_access.feed_delete && can_access.feed_edit
// .stamp <a href="/dash/c/form/FeedPost?id=#{item.id}" class="feed-edit-button" target="_blank">edit</a> | <a href="#recent" class="feed-delete-button" postid="#{item.id}">delete</a>
// .stamp <a href="#{app.url}feed##{item.id}" class="feed-edit-button">permalink</a>&nbsp;&nbsp;|&nbsp;&nbsp;#{item.date.toLocaleString()}
if feed_overflow
.row.mt-4
.col-12.text-center
a.button(href="/feed") view more