refactor: remove unbody service

This commit is contained in:
Hossein Mehrabi 2024-01-22 17:56:26 +03:30
parent 047f378e4d
commit 6d4f93f71c
No known key found for this signature in database
GPG Key ID: 45C04964191AFAA1
29 changed files with 5 additions and 26797 deletions

2
.env
View File

@ -1,5 +1,3 @@
UNBODY_API_KEY=
UNBODY_PROJECT_ID=
SIMPLECAST_ACCESS_TOKEN=
REVALIDATE_WEBHOOK_TOKEN=
DISCORD_LOGS_WEBHOOK_URL=

1
.gitignore vendored
View File

@ -39,3 +39,4 @@ next-env.d.ts
public/rss.xml
public/atom*.xml
public/images/placeholders/*
!public/images/placeholders/.gitkeep

View File

@ -7,10 +7,9 @@ ARG PORT=3000
EXPOSE ${PORT}
# Credentials
ARG UNBODY_PROJECT_ID
ARG UNBODY_API_KEY
ARG SIMPLECAST_ACCESS_TOKEN
ARG REVALIDATE_WEBHOOK_TOKEN
ARG STRAPI_API_KEY
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1

2
Jenkinsfile vendored
View File

@ -43,7 +43,7 @@ pipeline {
]) {
image = docker.build(
"${IMAGE_NAME}:${GIT_COMMIT.take(8)}",
["--build-arg='UNBODY_PROJECT_ID=${env.UNBODY_PROJECT_ID}'",
["--build-arg='STRAPI_API_KEY=${env.UNBODY_PROJECT_ID}'",
"--build-arg='UNBODY_API_KEY=${env.UNBODY_API_KEY}'",
"--build-arg='SIMPLECAST_ACCESS_TOKEN=${SIMPLECAST_ACCESS_TOKEN}'",
"--build-arg='REVALIDATE_WEBHOOK_TOKEN=${REVALIDATE_WEBHOOK_TOKEN}'",

View File

@ -12,7 +12,7 @@ The repository for [press.logos.co](https://press.logos.co/) website.
- Emotion: CSS-in-JS
- [Unbody](https://unbody.io/) : CMS
- [Strapi](https://strapi.io/) : CMS
## Environment Variables
@ -20,8 +20,6 @@ The repository for [press.logos.co](https://press.logos.co/) website.
Please check the environment values in `.env` located in the root directory.
```
UNBODY_API_KEY=
UNBODY_PROJECT_ID=
SIMPLECAST_ACCESS_TOKEN=
REVALIDATE_WEBHOOK_TOKEN=
NEXT_PUBLIC_SITE_URL=https://press.logos.co
@ -30,8 +28,6 @@ FATHOM_SITE_ID=
This is a template for `.env.local`, which is included in `.gitignore`.
You can obtain an Unbody API key and project ID through your [Unbody project](https://app.unbody.io/).
To find the Simplecast access token, follow these steps on the Simplecast dashboard:
1. Click the gear button in the top-right corner.

View File

@ -1,30 +0,0 @@
type TocItem {
level: Int!
tag: String!
href: String!
title: String!
blockIndex: Int!
}
type Mention {
name: String!
emailAddress: String!
}
type Footnote {
index: Int!
id: String!
refId: String!
refValue: String!
valueHTML: String!
valueText: String!
}
extend type GoogleDoc {
mentionsObj: [Mention]!
tocObj: [TocItem]!
}
extend type TextBlock {
footnotesObj: [Footnote]
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -27,12 +27,6 @@ Page.getLayout = function getLayout(page: React.ReactNode) {
}
export const getStaticProps: GetStaticProps<PageProps> = async () => {
// const { data: tags = [] } = await unbodyApi.getTopics(true)
// const { data: highlighted } = await unbodyApi.getHighlightedPosts()
// const { data: latest } = await unbodyApi.getRecentPosts({
// skip: 0,
// limit: 15,
// })
const { data: latest } = await strapiApi.getRecentPosts({
highlighted: 'exclude',
limit: 15,

View File

@ -1,70 +0,0 @@
import { LPE } from '../../../types/lpe.types'
import { calcReadingTime } from '../../../utils/string.utils'
import { UnbodyResGoogleDocData } from '../unbody.types'
import { UnbodyDataTypeConfig } from './types'
export const ArticleDataType: UnbodyDataTypeConfig<
UnbodyResGoogleDocData,
LPE.Article.Data
> = {
key: 'ArticleDocument',
objectType: 'GoogleDoc',
classes: ['article', 'podcast', 'show', 'episode', 'document'],
isMatch: (helpers, data, original, root) =>
data.pathString.includes('/Articles/') ||
data.pathString.includes('/Podcasts/'),
transform: async (helpers, data) => {
const textBlock = helpers.dataTypes.get({
objectType: 'TextBlock',
})
const imageBlock = helpers.dataTypes.get({
objectType: 'ImageBlock',
})
const blocks =
await helpers.dataTypes.transformMany<LPE.Article.ContentBlock>(
[...textBlock, ...imageBlock],
[...(data.blocks || [])]
.filter(
(block) =>
block.__typename === 'ImageBlock' || !!block.html || !!block.text,
)
.sort((a, b) => a.order - b.order),
data,
)
const readingTime = calcReadingTime(
(data.blocks || [])
.filter((block) => block.__typename === 'TextBlock')
.map((block) => ('text' in block ? block.text : ''))
.join(' '),
)
return {
id: data._additional.id,
slug: data.slug,
tags: data.tags ?? [],
title: data.title,
subtitle: data.subtitle || '',
summary: data.summary || '',
authors: data.mentionsObj ?? [],
createdAt: data.createdAt || null,
modifiedAt: data.modifiedAt || null,
content: blocks,
coverImage:
(blocks.find((block) =>
(block.labels || []).includes(
LPE.Article.ContentBlockLabels.CoverImage,
),
) as LPE.Article.ImageBlock) || null,
readingTime,
toc: data.tocObj ?? [],
featured: data.path.includes('featured'),
highlighted: data.path.includes('highlighted'),
isDraft: data.path.includes('draft'),
type: LPE.PostTypes.Article,
} as any
},
}

View File

@ -1,33 +0,0 @@
import { ArticleBlocksOrders } from '../../../configs/data.configs'
import { LPE } from '../../../types/lpe.types'
import {
UnbodyResGoogleDocData,
UnbodyResImageBlockData,
} from '../unbody.types'
import { UnbodyDataTypeConfig } from './types'
export const ArticleImageBlockDataType: UnbodyDataTypeConfig<
LPE.Article.ImageBlock,
LPE.Article.ImageBlock,
UnbodyResImageBlockData,
UnbodyResGoogleDocData
> = {
key: 'ArticleImageBlock',
objectType: 'ImageBlock',
classes: ['article'],
isMatch: (helpers, data, original, root) =>
data.type === 'image' && (root?.path || []).includes('Articles'),
transform: (helpers, data, original, root) => {
if (!root) return data
if (data.order === ArticleBlocksOrders.cover)
return {
...data,
labels: [...data.labels, LPE.Article.ContentBlockLabels.CoverImage],
}
return data
},
}

View File

@ -1,94 +0,0 @@
import { LPE } from '../../../types/lpe.types'
import { UnbodyHelpers } from '../unbody.helpers'
import {
UnbodyResSearchGoogleDocData,
UnbodyResSearchResultImageBlockData,
UnbodyResSearchResultTextBlockData,
} from '../unbody.types'
import { UnbodyDataTypeConfig } from './types'
export const ArticleSearchResultItemDataType: UnbodyDataTypeConfig<
| UnbodyResSearchGoogleDocData
| UnbodyResSearchResultTextBlockData
| UnbodyResSearchResultImageBlockData,
LPE.Search.ResultItem,
| UnbodyResSearchGoogleDocData
| UnbodyResSearchResultTextBlockData
| UnbodyResSearchResultImageBlockData,
undefined,
{
query: string
tags: string[]
shows: LPE.Podcast.Show[]
}
> = {
key: 'ArticleSearchResultItem',
objectType: 'GoogleDoc',
classes: ['article', 'search'],
isMatch: (helpers, data, original, root) =>
data.__typename === 'GoogleDoc'
? !root &&
(data.pathString.includes('/Articles/') ||
data.pathString.includes('/Podcasts/'))
: data.__typename === 'ImageBlock' || data.__typename === 'TextBlock',
transform: async (helpers, data, original, root, context) => {
const { query = '', tags = [] } = context ?? {}
if (data.__typename === 'GoogleDoc') {
const score = UnbodyHelpers.resolveScore(data._additional)
const transformers = helpers.dataTypes.get({ objectType: 'GoogleDoc' })
const transformed = await helpers.dataTypes.transform<LPE.Post.Document>(
transformers,
data,
undefined,
{ ...context },
)
return {
score,
data: transformed,
type: transformed.type,
}
}
const score =
query.length > 0 || tags.length > 0
? UnbodyHelpers.resolveScore(data._additional)
: 0
const transformers = helpers.dataTypes.get({ objectType: 'GoogleDoc' })
const document = await helpers.dataTypes.transform<LPE.Post.Document>(
transformers,
'document' in data && data.document?.[0],
undefined,
{
...context,
},
)
const transformed = await helpers.dataTypes.transform<
LPE.Post.ContentBlock<any>
>(
[
helpers.dataTypes.getOne({
objectType: 'TextBlock',
classes: 'article',
})!,
helpers.dataTypes.getOne({
objectType: 'ImageBlock',
classes: 'article',
})!,
],
data,
)
return {
score,
data: { ...transformed, document } as any,
type: transformed.type,
}
},
}

View File

@ -1,53 +0,0 @@
import { LPE } from '../../../types/lpe.types'
import { similarity } from '../../../utils/string.utils'
import { UnbodyResGoogleDocData, UnbodyResTextBlockData } from '../unbody.types'
import { UnbodyDataTypeConfig } from './types'
export const ArticleTextBlockDataType: UnbodyDataTypeConfig<
LPE.Article.TextBlock,
LPE.Article.TextBlock,
UnbodyResTextBlockData,
UnbodyResGoogleDocData
> = {
key: 'ArticleTextBlock',
objectType: 'TextBlock',
classes: ['article'],
isMatch: (helpers, data, original, root) =>
data.type === 'text' && (root?.path || []).includes('Articles'),
transform: (helpers, data, original, root) => {
if (!root) return data
const { text = '', html = '', classNames = [] } = original
const { summary, tags = [], mentionsObj: mentions = [] } = root
const labels: LPE.Article.ContentBlockLabel[] = []
const isTitle = classNames.includes('title')
const isSubtitle = classNames.includes('subtitle')
const isAuthor =
similarity(text, mentions.map((m) => m.name).join('')) > 0.8
const isSummary = summary === text
const isTag = similarity(text, tags.map((t) => `#${t}`).join(' ')) > 0.8
//TODO this is a hack to remove the footnotes from the body
// we should find a better way to do this
const isFootnotes = html.match(`<a href="#ftnt_ref`)?.length
isTitle && labels.push(LPE.Article.ContentBlockLabels.Title)
isSubtitle && labels.push(LPE.Article.ContentBlockLabels.Subtitle)
isAuthor && labels.push(LPE.Article.ContentBlockLabels.Authors)
isSummary && labels.push(LPE.Article.ContentBlockLabels.Summary)
isTag && labels.push(LPE.Article.ContentBlockLabels.Tags)
isFootnotes && labels.push(LPE.Article.ContentBlockLabels.Footnote)
return {
...data,
labels: [...data.labels, ...labels],
}
},
}

View File

@ -1,32 +0,0 @@
import { LPE } from '../../../types/lpe.types'
import {
UnbodyResGoogleDocData,
UnbodyResImageBlockData,
} from '../unbody.types'
import { UnbodyDataTypeConfig } from './types'
export const ImageBlockDataType: UnbodyDataTypeConfig<
UnbodyResImageBlockData,
LPE.Article.ImageBlock,
UnbodyResImageBlockData,
UnbodyResGoogleDocData
> = {
key: 'ImageBlock',
objectType: 'ImageBlock',
classes: ['article'],
isMatch: (helpers, data, original, root) => data.__typename === 'ImageBlock',
transform: (helpers, data, original, root) => {
return {
id: data?._additional?.id || `${data.order}`,
type: 'image',
alt: data.alt,
url: data.url,
height: data.height,
order: data.order,
width: data.width,
labels: [],
}
},
}

View File

@ -1,256 +0,0 @@
import parseISO from 'date-fns/parseISO'
import { LPE } from '../../../types/lpe.types'
import { settle } from '../../../utils/promise.utils'
import { simplecastApi } from '../../simplecast.service'
import { UnbodyResGoogleDocData } from '../unbody.types'
import { UnbodyDataTypeConfig } from './types'
export const PodcastEpisodeDataType: UnbodyDataTypeConfig<
LPE.Article.Data,
LPE.Podcast.Document,
UnbodyResGoogleDocData,
any,
| {
shows: LPE.Podcast.Show[]
parseContent?: boolean
}
| undefined
> = {
key: 'PodcastEpisodeDocument',
objectType: 'GoogleDoc',
classes: ['podcast', 'episode', 'document'],
isMatch: (helpers, data, original) =>
original
? original.pathString.includes('/Podcasts/') &&
/^ep\d+-\d{8}-.*/.test(original.slug)
: false,
transform: async (helpers, data, original, root, context) => {
const { shows = [] } = context ?? {}
const show = shows.find((show) => show.slug === original.path[2])
const name = original.path[original.path.length - 1]
const [ep, date] = name.split('-')
const slug = original.slug.slice(`${ep}-${date}-`.length)
const publishedAt = parseISO(date)
const episodeNumber = parseInt(ep.slice(2), 10)
const coverImage = data.content.find(
(block) => block.type === 'image' && block.order < 20,
) as LPE.Podcast.Metadata['coverImage']
const channels: LPE.Podcast.Content['channels'] = []
const credits: LPE.Podcast.Content['credits'] = []
const transcription: LPE.Podcast.Content['transcription'] = []
const content: LPE.Podcast.Content['content'] = []
const allBlocks = data.content.filter((block) => {
if (
((block.type === 'text' && block.html) || '').match(
`<a href="#ftnt_ref`,
)?.length
)
return false
return true
})
if (context?.parseContent) {
const sections = findSections(
['Credits', 'Content', 'Timestamps', 'Transcription'],
allBlocks,
)
const textBlocks = allBlocks.filter(
(block) => block.type === 'text',
) as LPE.Post.TextBlock[]
sections.forEach((section) => {
switch (section.name) {
case 'Credits': {
credits.push(
...(section.blocks.filter(
(block) => block.type === 'text',
) as LPE.Post.TextBlock[]),
)
break
}
case 'Content':
case 'Timestamps':
case 'Transcription': {
content.push(...section.blocks)
break
}
case 'Transcriptions': {
break
}
}
})
channels.push(...(await getDistributionChannels(textBlocks)))
}
return {
id: data.id,
slug,
title: data.title,
authors: data.authors,
description: data.summary,
modifiedAt: data.modifiedAt || '',
publishedAt: publishedAt.toJSON(),
episodeNumber,
tags: data.tags,
credits,
content,
transcription,
channels,
...(show ? { show } : {}),
...(show ? { showId: show.id } : {}),
...(coverImage ? { coverImage } : {}),
highlighted: data.highlighted,
isDraft: data.isDraft,
type: LPE.PostTypes.Podcast,
} as any
},
}
const getDistributionChannels = async (blocks: LPE.Post.TextBlock[]) => {
const channels: LPE.Podcast.Channel[] = []
{
const youtubeUrlRegex =
/(https?\:\/\/)?((www\.)?youtube\.com|youtu\.?be)\/[^ "]+/gi
for (const block of blocks) {
const match = (block.html || '').match(youtubeUrlRegex)
if (match && match.length) {
const url = match[0]
channels.push({
name: LPE.Podcast.ChannelNames.Youtube,
url,
})
continue
}
}
}
const linkBlocks = blocks.filter((block) =>
/^(https):\/\/[^ "]+$/.test((block.text || '').trim()),
)
for (const block of linkBlocks) {
const { text } = block
const url = text
const name = [
[/spotify\.com/i, LPE.Podcast.ChannelNames.Spotify],
[/podcasts\.google\.com/i, LPE.Podcast.ChannelNames.GooglePodcasts],
[/podcasts\.apple\.com/i, LPE.Podcast.ChannelNames.ApplePodcasts],
[/simplecast\.com/i, LPE.Podcast.ChannelNames.Simplecast],
].find(
([reg, name]) => url.match(reg)?.length,
)?.[1] as LPE.Podcast.ChannelName
if (!name) continue
switch (name) {
case LPE.Podcast.ChannelNames.Simplecast: {
if (!simplecastApi.isValidPlayerUrl(url)) {
console.error('invalid Simplecast player url!')
continue
}
const episodeId = simplecastApi.extractEpisodeIdFromUrl(url)
if (!episodeId) {
console.error('invalid Simplecast player url!')
continue
}
const [res, err] = await settle(() =>
simplecastApi.getEpisode({ id: episodeId }),
)
if (err) {
console.error('failed to fetch Simplecast episode ', url)
console.error(err)
continue
}
const data: LPE.Podcast.SimplecastChannelData = {
duration: res.duration,
audioFileUrl: res.ad_free_audio_file_url ?? res.audio_file?.url,
}
channels.push({
name,
url,
data,
})
break
}
default: {
channels.push({
name,
url,
})
}
}
}
return channels
}
const findSections = (
names: string[],
blocks: LPE.Post.ContentBlock[],
): {
name: string
start: number
end: number
blocks: LPE.Post.ContentBlock[]
}[] => {
let sections: {
name: string
start: number
end: number
blocks: LPE.Post.ContentBlock[]
}[] = names.map((name) => ({ name, start: -1, end: -1, blocks: [] }))
blocks.forEach((block, index) => {
const { type, ...rest } = block
if (block.type === 'text') {
const sectionIndex = sections.findIndex(
({ name }) => block.text.trim() === `[${name}]`,
)
if (sectionIndex === -1) return
const section = sections[sectionIndex]
section.start = index
}
})
sections = [...sections]
.sort((a, b) => (a.start < b.start ? -1 : 1))
.filter((section) => section.start > -1)
for (let i = 0; i < sections.length; i++) {
const section = sections[i]
const nextSection = sections[i + 1]
section.end = nextSection ? nextSection.start - 1 : blocks.length - 1
section.blocks = blocks.slice(section.start + 1, section.end + 1)
}
return sections
}

View File

@ -1,53 +0,0 @@
import { LPE } from '../../../types/lpe.types'
import { getPostLink } from '../../../utils/route.utils'
import { UnbodyResGoogleDocData } from '../unbody.types'
import { UnbodyDataTypeConfig } from './types'
export const PodcastShowDataType: UnbodyDataTypeConfig<
LPE.Article.Data,
LPE.Podcast.Show,
UnbodyResGoogleDocData,
any,
| {
numberOfEpisodes: number
}
| undefined
> = {
key: 'PodcastShowDocument',
objectType: 'GoogleDoc',
classes: ['podcast', 'show', 'document'],
isMatch: (helpers, data, original) =>
original
? original.pathString.includes('/Podcasts/') && original.slug === 'index'
: false,
transform: async (helpers, data, original, root, context) => {
if (!original) return data as any
const description = data.content.find(
(block) =>
block.labels.length === 0 && block.type === 'text' && block.order > 2,
)
const showSlug = original.path[2]
return {
id: data.id,
slug: showSlug,
title: data.title,
numberOfEpisodes: context?.numberOfEpisodes || 0,
hosts: data.authors,
url: getPostLink('podcast', { showSlug }),
description: (description?.type === 'text' && description.html) || '',
descriptionText: (description?.type === 'text' && description.text) || '',
logo: {
alt: data.title,
width: 24,
height: 24,
url: `/podcasts/${showSlug}-logo.svg`,
},
episodes: [],
}
},
}

View File

@ -1,44 +0,0 @@
import { LPE } from '../../../types/lpe.types'
import { UnbodyResGoogleDocData } from '../unbody.types'
import { UnbodyDataTypeConfig } from './types'
export const StaticPageDataType: UnbodyDataTypeConfig<
UnbodyResGoogleDocData,
LPE.StaticPage.Document
> = {
key: 'StaticPageDocument',
objectType: 'GoogleDoc',
classes: ['static-page', 'document'],
isMatch: (helpers, data) =>
!!data?.pathString && data.pathString.includes('/Static pages/'),
transform: async (helpers, data) => {
const textBlock = helpers.dataTypes.get({
objectType: 'TextBlock',
})
const imageBlock = helpers.dataTypes.get({
objectType: 'ImageBlock',
})
const blocks =
await helpers.dataTypes.transformMany<LPE.Article.ContentBlock>(
[...textBlock, ...imageBlock],
[...(data.blocks || [])].sort((a, b) => a.order - b.order),
data,
)
return {
id: data._additional.id,
slug: data.slug,
title: data.title,
subtitle: data.subtitle || '',
summary: data.summary || '',
createdAt: data.createdAt || null,
modifiedAt: data.modifiedAt || null,
content: blocks,
type: 'static_page',
isDraft: data.pathString.includes('/draft/'),
}
},
}

View File

@ -1,97 +0,0 @@
import { LPE } from '../../../types/lpe.types'
import { setAttributeOnHTML } from '../../../utils/html.utils'
import { convertToIframe } from '../../../utils/string.utils'
import { UnbodyResGoogleDocData, UnbodyResTextBlockData } from '../unbody.types'
import { UnbodyDataTypeConfig } from './types'
export const TextBlockDataType: UnbodyDataTypeConfig<
UnbodyResTextBlockData,
LPE.Article.TextBlock,
UnbodyResTextBlockData,
UnbodyResGoogleDocData
> = {
key: 'TextBlock',
objectType: 'TextBlock',
classes: ['article'],
isMatch: (helpers, data, original, root) => data.__typename === 'TextBlock',
transform: (helpers, data, original, root) => {
let { text = '', html = '' } = data
const labels: LPE.Post.ContentBlockLabel[] = []
let embed: LPE.Post.TextBlockEmbed | null = null
if (text.length > 0 || html.length > 0) {
const isLink =
/^<p[^>]*>(<span[^>]*>[\s]*<\/span>)*<span[^>]*><a[^>]*href="(https):\/\/[^ "]+"[^>]*>.*<\/a><\/span>(<span[^>]*>(\s|(<br(\/)?>))*<\/span>)*<\/p>$/.test(
html,
)
const isIframe = /<iframe[^>]*>(?:<\/iframe>|[^]*?<\/iframe>)/.test(text)
if (isLink) {
labels.push(LPE.Post.ContentBlockLabels.LinkOnly)
const youtube = html.match(
/(https?\:\/\/)?((www\.)?youtube\.com|youtu\.?be)\/[^ "]+/gi,
)
const simplecast = html.match(
/(https?\:\/\/)?((player\.)?simplecast\.com)\/[^ "]+/gi,
)
const label = youtube?.[0]
? LPE.Post.ContentBlockLabels.YoutubeEmbed
: LPE.Post.ContentBlockLabels.SimplecastEmbed
const src = youtube?.[0] || simplecast?.[0]
if (src && src.length > 0) {
labels.push(label)
labels.push(LPE.Post.ContentBlockLabels.Embed)
embed = {
src,
html: convertToIframe(src),
}
}
}
if (isIframe) {
const src = text.match(/(?<=src=").*?(?=[\?"])/g)?.[0]
if (src) {
labels.push(LPE.Post.ContentBlockLabels.Embed)
embed = {
html: text,
src,
}
}
}
}
// set target="_blank" on anchor elements
{
const matches = Array.from(
html.matchAll(/<a[^>]*href="http[^>]*"[^>]*>/gi),
)
for (const match of matches) {
const [anchorHTML] = match
const newAnchorHTML = setAttributeOnHTML(anchorHTML, 'target', '_blank')
html = html.replace(anchorHTML, newAnchorHTML)
}
}
return {
id: data?._additional?.id || `${data.order}`,
type: 'text',
html,
text: data.text || '',
classNames: data.classNames,
footnotes: data.footnotesObj,
order: data.order,
tagName: data.tagName,
labels,
...(embed ? { embed } : {}),
}
},
}

View File

@ -1,92 +0,0 @@
import {
UnbodyDataTypeClass,
UnbodyDataTypeConfig,
UnbodyDataTypeConfigHelpers,
UnbodyDataTypeKey,
} from './types'
export class UnbodyDataTypes {
private dataTypes: UnbodyDataTypeConfig[] = []
private helpers: UnbodyDataTypeConfigHelpers
constructor(dataTypes: UnbodyDataTypeConfig<any>[]) {
this.dataTypes = dataTypes
this.helpers = {
dataTypes: this,
}
}
getOne = ({
key,
classes,
objectType,
}: {
key?: UnbodyDataTypeKey
classes?: UnbodyDataTypeClass | UnbodyDataTypeClass[]
objectType?: UnbodyDataTypeConfig['objectType']
}) => {
let dataTypes = this.dataTypes
if (key) {
return dataTypes.find((doc) => doc.key === key)
}
return this.get({ classes, objectType })[0]
}
get = ({
classes: _classes,
objectType,
}: {
classes?: UnbodyDataTypeClass | UnbodyDataTypeClass[]
objectType?: UnbodyDataTypeConfig['objectType']
}) => {
let dataTypes = this.dataTypes
if (objectType)
dataTypes = dataTypes.filter(
(dataType) => dataType.objectType === objectType,
)
const classes = !_classes
? []
: Array.isArray(_classes)
? _classes
: [_classes]
if (classes.length > 0)
dataTypes = dataTypes.filter((dataType) =>
classes.every((cls) => dataType.classes.includes(cls)),
)
return dataTypes
}
transform = async <O = any, T = any>(
pipeline: UnbodyDataTypeConfig[],
data: T,
root?: any,
context?: any,
): Promise<O> => {
let obj = data
for (const dataType of pipeline) {
if (dataType.isMatch(this.helpers, obj, data, root, context)) {
obj = await dataType.transform(this.helpers, obj, data, root, context)
}
}
return obj as O | Promise<O>
}
transformMany = async <O = any, T = any>(
pipeline: UnbodyDataTypeConfig[],
data: T[],
root?: any,
context?: any,
): Promise<O[]> => {
return Promise.all(
data.map((d) => this.transform<O, T>(pipeline, d, root, context)),
)
}
}

View File

@ -1,22 +0,0 @@
import { ArticleDataType } from './ArticleDocument.dataType'
import { ArticleImageBlockDataType } from './ArticleImageBlock.dataType'
import { ArticleSearchResultItemDataType } from './ArticleSearchResultItem.dataType'
import { ArticleTextBlockDataType } from './ArticleTextBlock.dataType'
import { ImageBlockDataType } from './ImageBlock.dataType'
import { PodcastEpisodeDataType } from './PodcastEpisodeDocument.dataType'
import { PodcastShowDataType } from './PodcastShowDocument.dataType'
import { StaticPageDataType } from './StaticPageDocument.dataType'
import { TextBlockDataType } from './TextBlock.dataType'
import { UnbodyDataTypes } from './UnbodyDataTypes'
export const unbodyDataTypes = new UnbodyDataTypes([
ArticleDataType,
TextBlockDataType,
ImageBlockDataType,
ArticleTextBlockDataType,
ArticleImageBlockDataType,
ArticleSearchResultItemDataType,
PodcastShowDataType,
PodcastEpisodeDataType,
StaticPageDataType,
])

View File

@ -1,7 +0,0 @@
export * from './ArticleDocument.dataType'
export * from './ArticleImageBlock.dataType'
export * from './ArticleTextBlock.dataType'
export * from './TextBlock.dataType'
export * from './UnbodyDataTypes'
export * from './dataTypes'
export * from './types'

View File

@ -1,69 +0,0 @@
import {
GoogleDoc,
ImageBlock,
TextBlock,
} from '../../../lib/unbody/unbody.generated'
import { UnbodyDataTypes } from './UnbodyDataTypes'
export type UnbodyDataTypeConfig<
D = any,
T = any,
O = any,
R = any,
C = any,
> = {
key: UnbodyDataTypeKey
classes: UnbodyDataTypeClass[]
objectType:
| GoogleDoc['__typename']
| TextBlock['__typename']
| ImageBlock['__typename']
isMatch: (
helpers: UnbodyDataTypeConfigHelpers,
object: D,
original: O,
root: R | undefined,
context: C,
) => boolean
transform: (
helpers: UnbodyDataTypeConfigHelpers,
object: D,
original: O,
root: R | undefined,
context: C,
) => T | Promise<T>
}
export type UnbodyDataTypeConfigHelpers = {
dataTypes: UnbodyDataTypes
}
export const UnbodyDataTypeKeys = {
TextBlock: 'TextBlock',
ImageBlock: 'ImageBlock',
ArticleDocument: 'ArticleDocument',
ArticleTextBlock: 'ArticleTextBlock',
ArticleImageBlock: 'ArticleImageBlock',
ArticleSearchResultItem: 'ArticleSearchResultItem',
PodcastShowDocument: 'PodcastShowDocument',
PodcastEpisodeDocument: 'PodcastEpisodeDocument',
StaticPageDocument: 'StaticPageDocument',
} as const
export type UnbodyDataTypeKey =
(typeof UnbodyDataTypeKeys)[keyof typeof UnbodyDataTypeKeys]
export const UnbodyDataTypeClasses = {
Article: 'article',
Podcast: 'podcast',
Show: 'show',
Episode: 'episode',
Document: 'document',
Search: 'search',
StaticPage: 'static-page',
} as const
export type UnbodyDataTypeClass =
(typeof UnbodyDataTypeClasses)[keyof typeof UnbodyDataTypeClasses]

View File

@ -1 +0,0 @@
export * from './unbody.service'

View File

@ -1,151 +0,0 @@
import { gql } from 'graphql-request'
export const TEXT_BLOCK_FRAGMENT_COMMON = gql`
fragment TextBlockCommon on TextBlock {
footnotes
footnotesObj @client(always: true) {
index
id
refId
refValue
valueHTML
valueText
}
html
order
text
tagName
classNames
__typename
_additional {
id
}
}
`
export const IMAGE_BLOCK_FRAGMENT_COMMON = gql`
fragment ImageBlockCommon on ImageBlock {
url
alt
order
width
height
__typename
_additional {
id
}
}
`
export const GOOGLE_DOC_FRAGMENT_COMMON = gql`
fragment GoogleDocCommon on GoogleDoc {
sourceId
title
subtitle
summary
tags
createdAt
modifiedAt
slug
path
pathString
_additional {
id
}
}
`
export const GOOGLE_DOC_FRAGMENT_MENTIONS = gql`
fragment GoogleDocMentions on GoogleDoc {
mentions
mentionsObj @client(always: true) {
name
emailAddress
}
}
`
export const GOOGLE_DOC_FRAGMENT_TOC = gql`
fragment GoogleDocTOC on GoogleDoc {
toc
tocObj @client(always: true) {
level
tag
href
title
blockIndex
}
}
`
export const SEARCH_IMAGE_BLOCK_FRAGMENT = gql`
fragment SearchImageBlock on ImageBlock {
__typename
...ImageBlockCommon
_additional {
id
score
certainty
}
document {
... on GoogleDoc {
...GoogleDocCommon
...GoogleDocMentions
}
}
}
${IMAGE_BLOCK_FRAGMENT_COMMON}
${GOOGLE_DOC_FRAGMENT_COMMON}
${GOOGLE_DOC_FRAGMENT_MENTIONS}
`
export const SEARCH_TEXT_BLOCK_FRAGMENT = gql`
fragment SearchTextBlock on TextBlock {
...TextBlockCommon
_additional {
id
score
certainty
}
document {
... on GoogleDoc {
...GoogleDocCommon
...GoogleDocMentions
}
}
}
${TEXT_BLOCK_FRAGMENT_COMMON}
${GOOGLE_DOC_FRAGMENT_COMMON}
${GOOGLE_DOC_FRAGMENT_MENTIONS}
`
export const SEARCH_GOOGLE_DOC_FRAGMENT = gql`
fragment SearchGoogleDoc on GoogleDoc {
...GoogleDocCommon
...GoogleDocMentions
blocks {
... on ImageBlock {
...ImageBlockCommon
}
}
_additional {
id
score
certainty
}
}
${TEXT_BLOCK_FRAGMENT_COMMON}
${IMAGE_BLOCK_FRAGMENT_COMMON}
${GOOGLE_DOC_FRAGMENT_COMMON}
${GOOGLE_DOC_FRAGMENT_MENTIONS}
`

View File

@ -1,111 +0,0 @@
import {
GetObjectsGoogleDocWhereInpObj,
GetObjectsGoogleDocWhereOperandsInpObj,
GoogleDocAdditional,
ImageBlockAdditional,
TextBlockAdditional,
} from '../../lib/unbody/unbody.generated'
export class UnbodyHelpers {
static resolveScore = (
_additional: Partial<
GoogleDocAdditional | ImageBlockAdditional | TextBlockAdditional
>,
): number =>
_additional?.certainty ||
(_additional?.score && parseFloat(_additional.score)) ||
0
static args = {
limit: (value: number): any => String(value),
skip: (value: number): any => String(value),
page: (skip: number, limit: number = 10) => ({
limit: this.args.limit(limit),
skip: this.args.skip(skip),
}),
wherePath: (
path: Array<string | null | undefined | false>,
key: string[] = ['path'],
) => {
const input = path.filter((p) => p && typeof p === 'string') as string[]
const paths: string[] = []
const or: string[] = []
const exclude: string[] = []
for (const p of input) {
if (p.startsWith('!')) exclude.push(p.slice(1))
else if (p.includes('|'))
or.push(...p.split('|').filter((s) => s.length > 0))
else paths.push(p)
}
return {
operator: 'And',
operands: [
...paths.map(
(p) =>
({
operator: 'Equal',
path: key,
valueString: p,
} as GetObjectsGoogleDocWhereInpObj),
),
...exclude.map(
(p) =>
({
operator: 'NotEqual',
path: key,
valueString: p,
} as GetObjectsGoogleDocWhereInpObj),
),
...(or.length
? [
{
operator: 'Or',
operands: or.map(
(p) =>
({
operator: 'Equal',
path: key,
valueString: p,
} as GetObjectsGoogleDocWhereInpObj),
),
} as GetObjectsGoogleDocWhereInpObj,
]
: []),
],
} as GetObjectsGoogleDocWhereInpObj
},
wherePublished: (value: boolean, path: string[] = ['pathString']) =>
({
operator: 'Or',
operands: value
? [
{
path,
operator: 'Like',
valueString: '/Articles/published/*',
},
]
: [],
} as GetObjectsGoogleDocWhereOperandsInpObj),
whereSlugIs: (slug: string, path = ['slug']) =>
({
operator: 'Equal',
path,
valueString: slug,
} as GetObjectsGoogleDocWhereOperandsInpObj),
whereSlugIsNot: (slug: string, path = ['slug']) =>
({
operator: 'NotEqual',
path: path,
valueString: slug,
} as GetObjectsGoogleDocWhereOperandsInpObj),
whereIdIsNot: (id: string) =>
({
operator: 'NotEqual',
path: ['id'],
valueString: id,
} as GetObjectsGoogleDocWhereOperandsInpObj),
}
}

View File

@ -1,237 +0,0 @@
import { gql } from '@apollo/client'
export const COUNT_DOCUMENTS_QUERY = gql`
query CountDocuments($filter: AggregateObjectsGoogleDocWhereInpObj) {
Aggregate {
GoogleDoc(where: $filter) {
meta {
count
}
}
}
}
`
export const GET_POSTS_QUERY = gql`
query GetPosts(
$filter: GetObjectsGoogleDocWhereInpObj
$sort: [GetObjectsGoogleDocSortInpObj]
$searchResult: Boolean = false
$nearText: Txt2VecC11yGetObjectsGoogleDocNearTextInpObj
$hybrid: GetObjectsGoogleDocHybridInpObj
$nearObject: GetObjectsGoogleDocNearObjectInpObj
$skip: Int = 0
$limit: Int = 10
$toc: Boolean = false
$mentions: Boolean = false
$textBlocks: Boolean = false
$imageBlocks: Boolean = false
$remoteId: Boolean = false
) {
Get {
GoogleDoc(
where: $filter
hybrid: $hybrid
nearText: $nearText
nearObject: $nearObject
sort: $sort
offset: $skip
limit: $limit
) {
_additional {
id
score @include(if: $searchResult)
distance @include(if: $searchResult)
certainty @include(if: $searchResult)
}
title
subtitle
summary
slug
tags
path
createdAt
modifiedAt
pathString
remoteId @include(if: $remoteId)
mentions @include(if: $mentions)
mentionsObj @client(always: true) @include(if: $mentions) {
name
emailAddress
}
toc @include(if: $toc)
tocObj @client(always: true) @include(if: $toc) {
level
tag
href
title
blockIndex
}
blocks {
... on ImageBlock @include(if: $imageBlocks) {
url
alt
order
width
height
__typename
_additional {
id
}
}
... on TextBlock @include(if: $textBlocks) {
footnotes
footnotesObj @client(always: true) {
index
id
refId
refValue
valueHTML
valueText
}
html
order
text
tagName
classNames
__typename
_additional {
id
}
}
}
}
}
}
`
export const GET_ALL_TOPICS_QUERY = gql`
query GetAllTopics($filter: AggregateObjectsGoogleDocWhereInpObj) {
Aggregate {
GoogleDoc(where: $filter, groupBy: "tags") {
groupedBy {
value
}
tags {
topOccurrences {
value
occurs
}
}
}
}
}
`
export const SEARCH_BLOCKS_QUERY = gql`
query SearchBlocks(
$limit: Int
$skip: Int
$textNearText: Txt2VecC11yGetObjectsTextBlockNearTextInpObj
$imageNearText: Txt2VecC11yGetObjectsImageBlockNearTextInpObj
$textFilter: GetObjectsTextBlockWhereInpObj
$imageFilter: GetObjectsImageBlockWhereInpObj
$textHybrid: GetObjectsTextBlockHybridInpObj
$imageHybrid: GetObjectsImageBlockHybridInpObj
$text: Boolean = true
$image: Boolean = true
) {
Get {
TextBlock(
where: $textFilter
nearText: $textNearText
hybrid: $textHybrid
limit: $limit
offset: $skip
) @include(if: $text) {
footnotes
footnotesObj @client(always: true) {
index
id
refId
refValue
valueHTML
valueText
}
html
order
text
tagName
classNames
__typename
document {
... on GoogleDoc {
sourceId
title
subtitle
summary
tags
createdAt
modifiedAt
slug
path
pathString
_additional {
id
}
mentions
mentionsObj @client(always: true) {
name
emailAddress
}
}
}
_additional {
certainty
score
id
}
}
ImageBlock(
where: $imageFilter
nearText: $imageNearText
hybrid: $imageHybrid
limit: $limit
offset: $skip
) @include(if: $image) {
url
alt
order
width
height
__typename
document {
... on GoogleDoc {
sourceId
title
subtitle
summary
tags
createdAt
modifiedAt
slug
path
pathString
_additional {
id
}
mentions
mentionsObj @client(always: true) {
name
emailAddress
}
}
}
_additional {
certainty
score
id
}
}
}
}
`

File diff suppressed because it is too large Load Diff

View File

@ -1,36 +0,0 @@
import {
GoogleDocCommonFragment,
GoogleDocMentionsFragment,
GoogleDocTocFragment,
ImageBlockCommonFragment,
SearchGoogleDocFragment,
SearchImageBlockFragment,
SearchTextBlockFragment,
TextBlockCommonFragment,
} from '../../lib/unbody/unbody.generated'
export type UnbodyResTextBlockData = TextBlockCommonFragment
export type UnbodyResImageBlockData = ImageBlockCommonFragment
export type UnbodyResGoogleDocData = GoogleDocCommonFragment &
GoogleDocMentionsFragment &
GoogleDocTocFragment & {
blocks: Array<UnbodyResTextBlockData | UnbodyResImageBlockData>
}
export type UnbodyResRelatedPostData = GoogleDocCommonFragment &
GoogleDocMentionsFragment
export type UnbodyResPostData = {
data: UnbodyResGoogleDocData
relatedArticles: UnbodyResRelatedPostData[]
articlesFromSameAuthors: UnbodyResRelatedPostData[]
}
export type UnbodyResSearchGoogleDocData = SearchGoogleDocFragment
export type UnbodyResSearchResultTextBlockData = SearchTextBlockFragment
export type UnbodyResSearchResultImageBlockData = SearchImageBlockFragment
export type ApiSearchResultItem =
| UnbodyResSearchGoogleDocData
| UnbodyResSearchResultTextBlockData
| UnbodyResSearchResultImageBlockData

View File

@ -31,11 +31,7 @@ export type SearchHookDataPayload = {
export type SearchResults = {
articles: SearchHook<LPE.Article.ContentBlock>
blocks: SearchHook<LPE.Article.ContentBlock>
search: (
query: string,
tags: string[],
docType: any, // TODO: @refactor UnbodyGraphQl.UnbodyDocumentTypeNames
) => Promise<void>
search: (query: string, tags: string[], docType: any) => Promise<void>
reset: (initialData: SearchHookDataPayload) => void
}