-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[SOA-15] Enable parsing of link previews (#16)
* feat 🎸 (be): add link metadata model, migration and computed post property * feat 🎸 (be): add response interfaces * feat 🎸 (be): adapt controller responses * styles 💅 : created link-preview component * fix ✅ (be): force update timestamps and fields without timezone * styles 💅 : improvements on link-preview component * fix ✅ (be): serialize post update response * fix ✅ (build): types nits
- Loading branch information
1 parent
3295ab8
commit 550e3fb
Showing
16 changed files
with
398 additions
and
69 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,3 +11,4 @@ DB_USER=postgres | |
DB_PASSWORD=root | ||
DB_DATABASE=social-adonis | ||
SENTRY_DSN=<your_dsn_url> | ||
LINK_PREVIEW_API=key |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,38 @@ | ||
import type { HttpContext } from '@adonisjs/core/http' | ||
import { inject } from '@adonisjs/core' | ||
import Post from '#models/post' | ||
import { PostResponse } from 'app/interfaces/post' | ||
import PostsService from '#services/posts_service' | ||
import { PageObject } from '@adonisjs/inertia/types' | ||
import { PaginatedResponse } from 'app/interfaces/pagination' | ||
|
||
@inject() | ||
export default class FeedController { | ||
constructor() {} | ||
async index(ctx: HttpContext) { | ||
constructor(private readonly postsService: PostsService) {} | ||
|
||
async index( | ||
ctx: HttpContext | ||
): Promise<string | PageObject<{ posts: PaginatedResponse<PostResponse> }>> { | ||
const page = ctx.request.qs().page || 1 | ||
const posts = await Post.query() | ||
.orderBy('updated_at', 'desc') | ||
.preload('user') | ||
.paginate(page, 10) | ||
return ctx.inertia.render('feed', { posts }) | ||
|
||
const data: PostResponse[] = [] | ||
|
||
for (const post of posts) { | ||
const resource = await this.postsService.serialize(post) | ||
data.push(resource) | ||
} | ||
|
||
const { meta } = posts.toJSON() | ||
|
||
return ctx.inertia.render('feed', { | ||
posts: { | ||
data, | ||
meta, | ||
}, | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export interface BaseEntity { | ||
updatedAt: string | ||
createdAt: string | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
export interface MetaResponse { | ||
total: number | ||
perPage: number | ||
currentPage: number | ||
lastPage: number | ||
firstPage: number | ||
firstPageUrl: string | ||
lastPageUrl: string | ||
nextPageUrl: string | null | ||
previousPageUrl: string | null | ||
} | ||
|
||
export interface PaginatedResponse<T> { | ||
data: T[] | ||
meta: MetaResponse | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { BaseEntity } from 'app/interfaces/base-entity' | ||
import { UUID } from 'crypto' | ||
import { UserResponse } from './user' | ||
|
||
export interface LinkMetadataJSONResponse { | ||
title: string | ||
description: string | ||
thumbnail: string | ||
} | ||
|
||
export interface LinkResponse { | ||
id: UUID | ||
link: string | ||
metadata: LinkMetadataJSONResponse | ||
} | ||
|
||
export interface PostResponse extends BaseEntity { | ||
id: UUID | ||
content: string | ||
user: UserResponse | ||
link: LinkResponse | null | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import { ModelObject } from '@adonisjs/lucid/types/model' | ||
|
||
export interface UserResponse extends ModelObject {} // TODO: Complete me. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import { DateTime } from 'luxon' | ||
import { BaseModel, column } from '@adonisjs/lucid/orm' | ||
import { LinkMetadataJSONResponse } from 'app/interfaces/post' | ||
import type { UUID } from 'crypto' | ||
|
||
export class LinkMetadataJSON { | ||
// TODO: Could benefit in adding class-validator? | ||
declare thumbnail: string | ||
declare title: string | ||
declare description: string | ||
|
||
constructor(data: LinkMetadataJSONResponse) { | ||
if ( | ||
typeof data.thumbnail !== 'string' || | ||
typeof data.title !== 'string' || | ||
typeof data.description !== 'string' | ||
) { | ||
throw new Error('Invalid link json metadata.') | ||
} | ||
|
||
Object.assign(this, data) | ||
} | ||
} | ||
|
||
export default class LinkMetadata extends BaseModel { | ||
@column({ isPrimary: true }) | ||
declare id: UUID | ||
|
||
@column() | ||
declare link: string | ||
|
||
@column() | ||
declare metadata: LinkMetadataJSON | ||
|
||
@column.dateTime({ autoCreate: true }) | ||
declare createdAt: DateTime | ||
|
||
@column.dateTime({ autoCreate: true, autoUpdate: true }) | ||
declare updatedAt: DateTime | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
import LinkMetadata from '#models/link-metadata' | ||
import { LinkResponse } from 'app/interfaces/post' | ||
import { differenceInHours } from 'date-fns' | ||
import env from '#start/env' | ||
|
||
export default class LinkParserService { | ||
private API_Link: string | ||
private API_Key: string | ||
|
||
constructor() { | ||
this.API_Link = 'https://api.linkpreview.net' | ||
this.API_Key = env.get('LINK_PREVIEW_API') | ||
} | ||
|
||
async get(link: string): Promise<{ | ||
title: string | ||
description: string | ||
image: string | ||
} | null> { | ||
const data = fetch(this.API_Link, { | ||
method: 'POST', | ||
headers: { | ||
'X-Linkpreview-Api-Key': this.API_Key, | ||
}, | ||
mode: 'cors', | ||
body: JSON.stringify({ q: link }), | ||
}) | ||
.then((res) => { | ||
if (res.status != 200) { | ||
console.log(res.status) | ||
throw new Error('something went wrong') | ||
} | ||
return res.json() | ||
}) | ||
.then((response: { title: string; description: string; image: string }) => { | ||
return { | ||
title: response.title, | ||
description: response.description, | ||
image: response.image, | ||
} | ||
}) | ||
.catch((error) => { | ||
console.log(error) | ||
return null | ||
}) | ||
|
||
return data | ||
} | ||
|
||
async store(link: string): Promise<LinkResponse | null> { | ||
const metadata = await this.get(link) | ||
if (!metadata) return null | ||
const record = await LinkMetadata.create({ | ||
link, | ||
metadata: { | ||
title: metadata.title, | ||
description: metadata.description, | ||
thumbnail: metadata.image, | ||
}, | ||
}) | ||
return this.serialize(record) | ||
} | ||
|
||
async update(record: LinkMetadata): Promise<LinkResponse | null> { | ||
const data = await this.get(record.link) | ||
record.metadata.thumbnail = data?.image || record.metadata.thumbnail | ||
record.metadata.title = data?.title || record.metadata.title | ||
record.metadata.description = data?.description || record.metadata.description | ||
record.enableForceUpdate() | ||
await record.save() | ||
return this.serialize(record) | ||
} | ||
|
||
async show(link: string | null): Promise<LinkResponse | null> { | ||
if (!link) return null | ||
const record = await LinkMetadata.findBy('link', link) | ||
let resource: LinkResponse | null | ||
if (record) { | ||
if (differenceInHours(new Date(), record?.updatedAt.toJSDate()) >= 12) { | ||
resource = await this.update(record) | ||
} else { | ||
resource = this.serialize(record) | ||
} | ||
} else { | ||
resource = await this.store(link) | ||
} | ||
return resource | ||
} | ||
|
||
private serialize(record: LinkMetadata): LinkResponse { | ||
return { | ||
id: record.id, | ||
link: record.link, | ||
metadata: record.metadata, | ||
} | ||
} | ||
} |
Oops, something went wrong.