-
-
Notifications
You must be signed in to change notification settings - Fork 3
Sync collection #74
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
crutchcorn
wants to merge
8
commits into
main
Choose a base branch
from
sync-collection
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Sync collection #74
Changes from 3 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
88a42a7
chore: WIP adding task
crutchcorn 38b56c1
chore: add more types
crutchcorn 18a065a
chore: more work on processor
crutchcorn 6390740
chore: handle authors properly
crutchcorn e8a73df
chore: initial tests
crutchcorn 4d33c30
fix: issues with path are now fixed
crutchcorn 0b85993
chore: fix eslint
crutchcorn d074e9f
chore: fix format
crutchcorn File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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,175 @@ | ||
| import { Tasks, env } from "@playfulprogramming/common"; | ||
| import { collectionData, db } from "@playfulprogramming/db"; | ||
| import * as github from "@playfulprogramming/github-api"; | ||
| import { createProcessor } from "../../createProcessor.ts"; | ||
| import { eq } from "drizzle-orm"; | ||
| import matter from "gray-matter"; | ||
| import { CollectionMetaSchema } from "./types.ts"; | ||
| import { Value } from "@sinclair/typebox/value"; | ||
| import sharp from "sharp"; | ||
| import { Readable } from "node:stream"; | ||
| import { s3 } from "@playfulprogramming/s3"; | ||
|
|
||
| function extractLocale(name: string) { | ||
| const match = name.match(/\.([a-z]+)\.md$/); | ||
| return match ? match[1] : "en"; | ||
| } | ||
|
|
||
|
|
||
| const IMAGE_SIZE_MAX = 2048; | ||
|
|
||
| async function processImg( | ||
| stream: ReadableStream<Uint8Array>, | ||
| uploadKey: string, | ||
| ) { | ||
| const pipeline = sharp() | ||
| .resize({ | ||
| width: IMAGE_SIZE_MAX, | ||
| height: IMAGE_SIZE_MAX, | ||
| fit: "inside", | ||
| }) | ||
| .jpeg({ mozjpeg: true }); | ||
|
|
||
| Readable.fromWeb(stream as never).pipe(pipeline); | ||
|
|
||
| const bucket = await s3.createBucket(env.S3_BUCKET); | ||
| await s3.upload(bucket, uploadKey, undefined, pipeline, "image/jpeg"); | ||
| } | ||
|
|
||
|
|
||
| export default createProcessor(Tasks.SYNC_COLLECTION, async (job, { signal }) => { | ||
| const authorId = job.data.author; | ||
| const collectionId = job.data.collection; | ||
|
|
||
| const collectionMetaUrl = new URL( | ||
| `content/${encodeURIComponent(authorId)}/collections/${encodeURIComponent(collectionId)}`, | ||
| "http://localhost", | ||
| ); | ||
|
|
||
| const collectionMetaResponse = await github.getContents({ | ||
| ref: job.data.ref, | ||
| path: collectionMetaUrl.pathname, | ||
| repoOwner: env.GITHUB_REPO_OWNER, | ||
| repoName: env.GITHUB_REPO_NAME, | ||
| signal, | ||
| }); | ||
|
|
||
| if (collectionMetaResponse.data === undefined) { | ||
| if (collectionMetaResponse.response.status == 404) { | ||
| console.log( | ||
| `Metadata for ${collectionId} (${collectionMetaUrl.pathname}) returned 404 - removing collection entry.`, | ||
| ); | ||
| await db.delete(collectionData).where(eq(collectionData.slug, collectionId)); | ||
| return; | ||
| } | ||
|
|
||
| throw new Error(`Unable to fetch collection data for ${collectionId}`); | ||
| } | ||
|
|
||
| if (!collectionMetaResponse.data.entries || !Array.isArray(collectionMetaResponse.data.entries)) { | ||
| throw new Error(`Unable to fetch collection data for ${collectionId}`); | ||
| } | ||
|
|
||
| type Entry = typeof collectionMetaResponse.data.entries[number] | ||
|
|
||
| const collectionEntries = collectionMetaResponse.data.entries.reduce( | ||
| (prev, | ||
| // entry.name is `index.md` and path is `content/{authorId}/collections/{collectionId}/index.md` | ||
| // We may have many locales in the future, so we need to check the path as well. | ||
| entry) => { | ||
| if (!(entry.name.startsWith("index") && entry.name.endsWith(".md"))) { | ||
| return prev; | ||
| } | ||
|
|
||
| prev.push({ entry, locale: extractLocale(entry.name) }) | ||
| return prev; | ||
| }, | ||
| [] as Array<{ entry: Entry, locale: string }> | ||
| ) | ||
|
|
||
| // TODO: Should Promise.all()? | ||
| // Check if coverImg or socialImg have changed since last edit, if so upload to S3 | ||
| for (let { entry, locale } of collectionEntries) { | ||
| const contentUrl = new URL( | ||
| entry.path, | ||
| "http://localhost", | ||
| ); | ||
|
|
||
| const contentResponse = await github.getContentsRaw({ | ||
| ref: job.data.ref, | ||
| path: contentUrl.pathname, | ||
| repoOwner: env.GITHUB_REPO_OWNER, | ||
| repoName: env.GITHUB_REPO_NAME, | ||
| signal, | ||
| }); | ||
|
|
||
| if (contentResponse.data === undefined) { | ||
| throw new Error(`Unable to fetch collection content for ${collectionId} locale ${locale}`); | ||
| } | ||
|
|
||
| const { data } = matter(contentResponse.data); | ||
| const collectionParsedData = Value.Parse(CollectionMetaSchema, data); | ||
|
|
||
| let coverImgKey: string | null = null; | ||
| let socialImgKey: string | null = null; | ||
| if (collectionParsedData.coverImg) { | ||
| const coverImgUrl = new URL(collectionParsedData.coverImg, collectionMetaUrl); | ||
| const { data: coverImgStream } = await github.getContentsRawStream({ | ||
| ref: job.data.ref, | ||
| path: coverImgUrl.pathname, | ||
| repoOwner: env.GITHUB_REPO_OWNER, | ||
| repoName: env.GITHUB_REPO_NAME, | ||
| signal, | ||
| }); | ||
|
|
||
| if (coverImgStream === null || typeof coverImgStream === "undefined") { | ||
| throw new Error( | ||
| `Unable to fetch cover image for ${collectionId} (${coverImgUrl.pathname})`, | ||
| ); | ||
| } | ||
|
|
||
| coverImgKey = `collections/${collectionId}/${locale}/cover.jpg`; | ||
| await processImg(coverImgStream, coverImgKey); | ||
| } | ||
|
|
||
| if (collectionParsedData.socialImg) { | ||
| const socialImgUrl = new URL(collectionParsedData.socialImg, collectionMetaUrl); | ||
| const { data: socialImgStream } = await github.getContentsRawStream({ | ||
| ref: job.data.ref, | ||
| path: socialImgUrl.pathname, | ||
| repoOwner: env.GITHUB_REPO_OWNER, | ||
| repoName: env.GITHUB_REPO_NAME, | ||
| signal, | ||
| }); | ||
|
|
||
| if (socialImgStream === null || typeof socialImgStream === "undefined") { | ||
| throw new Error( | ||
| `Unable to fetch social image for ${collectionId} (${socialImgUrl.pathname})`, | ||
| ); | ||
| } | ||
|
|
||
| socialImgKey = `collections/${collectionId}/${locale}/social.jpg`; | ||
| await processImg(socialImgStream, socialImgKey); | ||
| } | ||
|
|
||
| const result = { | ||
| slug: collectionId, | ||
| locale: locale, | ||
| title: collectionParsedData.title, | ||
| description: collectionParsedData.description, | ||
| coverImage: coverImgKey, | ||
| socialImage: socialImgKey, | ||
| meta: { | ||
| buttons: collectionParsedData.buttons, | ||
| tags: collectionParsedData.tags, | ||
| chapterList: collectionParsedData.chapterList, | ||
| } | ||
| }; | ||
|
|
||
| await db.insert(collectionData) | ||
| .values(result) | ||
| .onConflictDoUpdate({ target: [collectionData.slug, collectionData.locale], set: result }); | ||
|
|
||
| // TODO: How to handle authors? | ||
|
||
| } | ||
| }); | ||
This file contains hidden or 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,90 @@ | ||
| import { Type } from "@sinclair/typebox"; | ||
|
|
||
| const CollectionButtonSchema = Type.Object( | ||
| { | ||
| text: Type.String(), | ||
| url: Type.String(), | ||
| }, | ||
| { | ||
| additionalProperties: false, | ||
| examples: [ | ||
| { | ||
| text: "Learn More", | ||
| url: "https://example.com/learn-more", | ||
| }, | ||
| ], | ||
| }, | ||
| ); | ||
|
|
||
| const CollectionCurrentPost = Type.Object( | ||
| { | ||
| post: Type.String(), | ||
| }, | ||
| { | ||
| additionalProperties: false, | ||
| examples: [ | ||
| { | ||
| post: "abc123", | ||
| }, | ||
| ], | ||
| }, | ||
| ); | ||
|
|
||
| const CollectionFuturePost = Type.Object( | ||
| { | ||
| order: Type.Number(), | ||
| title: Type.String(), | ||
| description: Type.String({ default: "" }), | ||
| }, | ||
| { | ||
| additionalProperties: false, | ||
| examples: [ | ||
| { | ||
| order: 1, | ||
| title: "Chapter One", | ||
| description: "An introduction to chapter one.", | ||
| }, | ||
| ], | ||
| }, | ||
| ); | ||
|
|
||
| export const CollectionMetaSchema = Type.Object( | ||
| { | ||
| title: Type.String(), | ||
| description: Type.String({ default: "" }), | ||
| authors: Type.Optional(Type.Array(Type.String())), | ||
| coverImg: Type.String(), | ||
| socialImg: Type.Optional(Type.String()), | ||
| type: Type.Optional(Type.Literal("book")), | ||
| pageLayout: Type.Optional(Type.Literal("none")), | ||
| customChaptersText: Type.Optional(Type.String()), | ||
| tags: Type.Optional(Type.Array(Type.String())), | ||
| published: Type.String(), | ||
| noindex: Type.Optional(Type.Boolean({ default: false })), | ||
| version: Type.Optional(Type.String()), | ||
| upToDateSlug: Type.Optional(Type.String()), | ||
| buttons: Type.Optional(Type.Array(CollectionButtonSchema)), | ||
| chapterList: Type.Optional(Type.Array(Type.Union([CollectionCurrentPost, CollectionFuturePost]))), | ||
| }, | ||
| { | ||
| additionalProperties: false, | ||
| examples: [ | ||
| { | ||
| title: "My Collection", | ||
| description: "A collection of my favorite posts.", | ||
| coverImg: "./cover.jpg", | ||
| published: "2023-01-01T00:00:00Z", | ||
| tags: ["tag1", "tag2"], | ||
| buttons: [ | ||
| { | ||
| text: "Learn More", | ||
| url: "/learn-more", | ||
| }, | ||
| { | ||
| post: "abc123", | ||
| }, | ||
| ], | ||
| }, | ||
| ], | ||
| }, | ||
| ); |
This file contains hidden or 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 hidden or 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
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.