diff --git a/packages/core/migrations/1686773145661_user.mjs b/packages/core/migrations/1686773145661_user.mjs deleted file mode 100644 index 96fc677..0000000 --- a/packages/core/migrations/1686773145661_user.mjs +++ /dev/null @@ -1,23 +0,0 @@ -import { sql } from 'kysely'; - -/** - * @param db {Kysely} - */ -export async function up(db) { - await db.schema - .createTable('user') - .addColumn('id', 'text', (col) => col.primaryKey()) - .addColumn('email', 'text', (col) => col.notNull()) - .addColumn('password', 'text', (col) => col.notNull()) - .addColumn('created', 'timestamp', (col) => - col.notNull().defaultTo(sql`now()`) - ) - .execute(); -} - -/** - * @param db {Kysely} - */ -export async function down(db) { - await db.schema.dropTable('user').execute(); -} diff --git a/packages/core/migrations/1686773145663_analytic.mjs b/packages/core/migrations/1686773145663_analytic.mjs deleted file mode 100644 index 4dde888..0000000 --- a/packages/core/migrations/1686773145663_analytic.mjs +++ /dev/null @@ -1,40 +0,0 @@ -import { sql } from 'kysely'; - -/** - * @param db {Kysely} - */ -export async function up(db) { - await db.schema - .createTable('analytic') - .addColumn('id', 'text', (col) => col.primaryKey()) - .addColumn('visitor_id', 'text', (col) => col.notNull()) - .addColumn('name', 'text', (col) => col.notNull()) - .addColumn('url', 'text', (col) => col.notNull()) - .addColumn('browser_name', 'text', (col) => col.notNull()) - .addColumn('browser_version', 'text', (col) => col.notNull()) - .addColumn('browser_engine', 'text', (col) => col.notNull()) - .addColumn('device_model', 'text', (col) => col.notNull()) - .addColumn('device_type', 'text', (col) => col.notNull()) - .addColumn('device_vendor', 'text', (col) => col.notNull()) - .addColumn('os_name', 'text', (col) => col.notNull()) - .addColumn('os_version', 'text', (col) => col.notNull()) - .addColumn('metadata', 'jsonb', (col) => col.notNull()) - .addColumn('created', 'timestamp', (col) => - col.notNull().defaultTo(sql`now()`) - ) - .execute(); - - await db.schema - .createIndex('idx_analytic_created') - .on('analytic') - .column('created') - .execute(); -} - -/** - * @param db {Kysely} - */ -export async function down(db) { - await db.schema.dropIndex('idx_analytic_created').execute(); - await db.schema.dropTable('analytic').execute(); -} diff --git a/packages/core/migrations/1699294539936_post.mjs b/packages/core/migrations/1699294539936_post.mjs deleted file mode 100644 index 9144ca2..0000000 --- a/packages/core/migrations/1699294539936_post.mjs +++ /dev/null @@ -1,29 +0,0 @@ -import { sql } from 'kysely'; - -/** - * @param db {Kysely} - */ -export async function up(db) { - await db.schema - .createTable('post') - .addColumn('id', 'text', (col) => col.primaryKey()) - .addColumn('title', 'text', (col) => col.notNull()) - .addColumn('slug', 'text', (col) => col.notNull()) - .addColumn('abstract', 'text', (col) => col.notNull()) - .addColumn('content', 'text', (col) => col.notNull()) - .addColumn('views', 'integer', (col) => col.notNull().defaultTo(0)) - .addColumn('is_published', 'boolean') - .addColumn('published_on', 'text') - .addColumn('created', 'timestamp', (col) => - col.notNull().defaultTo(sql`now()`) - ) - .addColumn('updated', 'timestamp') - .execute(); -} - -/** - * @param db {Kysely} - */ -export async function down(db) { - await db.schema.dropTable('post').execute(); -} diff --git a/packages/core/migrations/1699295470484_tag.mjs b/packages/core/migrations/1699295470484_tag.mjs deleted file mode 100644 index 85d5276..0000000 --- a/packages/core/migrations/1699295470484_tag.mjs +++ /dev/null @@ -1,22 +0,0 @@ -import { sql } from 'kysely'; - -/** - * @param db {Kysely} - */ -export async function up(db) { - await db.schema - .createTable('tag') - .addColumn('id', 'text', (col) => col.primaryKey()) - .addColumn('name', 'text', (col) => col.notNull()) - .addColumn('created', 'timestamp', (col) => - col.notNull().defaultTo(sql`now()`) - ) - .execute(); -} - -/** - * @param db {Kysely} - */ -export async function down(db) { - await db.schema.dropTable('tag').execute(); -} diff --git a/packages/core/migrations/1699295478397_post_tag.mjs b/packages/core/migrations/1699295478397_post_tag.mjs deleted file mode 100644 index b33a4be..0000000 --- a/packages/core/migrations/1699295478397_post_tag.mjs +++ /dev/null @@ -1,23 +0,0 @@ -import { sql } from 'kysely'; - -/** - * @param db {Kysely} - */ -export async function up(db) { - await db.schema - .createTable('post_tag') - .addColumn('id', 'text', (col) => col.primaryKey()) - .addColumn('post_id', 'text', (col) => col.notNull()) - .addColumn('tag_id', 'text', (col) => col.notNull()) - .addColumn('created', 'timestamp', (col) => - col.notNull().defaultTo(sql`now()`) - ) - .execute(); -} - -/** - * @param db {Kysely} - */ -export async function down(db) { - await db.schema.dropTable('post_tag').execute(); -} diff --git a/packages/core/migrations/1702054746780_post_upvote.mjs b/packages/core/migrations/1702054746780_post_upvote.mjs deleted file mode 100644 index 0b08357..0000000 --- a/packages/core/migrations/1702054746780_post_upvote.mjs +++ /dev/null @@ -1,24 +0,0 @@ -import { sql } from 'kysely'; - -/** - * @param db {Kysely} - */ -export async function up(db) { - await db.schema - .createTable('post_upvote') - .addColumn('id', 'text', (col) => col.primaryKey()) - .addColumn('post_id', 'text', (col) => col.notNull()) - .addColumn('visitor_id', 'text', (col) => col.notNull()) - .addColumn('votes', 'integer', (col) => col.notNull().defaultTo(1)) - .addColumn('created', 'timestamp', (col) => - col.notNull().defaultTo(sql`now()`) - ) - .execute(); -} - -/** - * @param db {Kysely} - */ -export async function down(db) { - await db.schema.dropTable('post_upvote').execute(); -} diff --git a/packages/core/package.json b/packages/core/package.json index 1951a2d..7aa9a26 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -11,12 +11,7 @@ }, "dependencies": { "@aws-sdk/client-dynamodb": "^3.540.0", - "@aws-sdk/client-rds-data": "^3.485.0", "@aws-sdk/lib-dynamodb": "^3.540.0", - "aws-sdk": "^2.1589.0", - "kysely": "^0.27.2", - "kysely-data-api": "^0.2.1", - "pg": "^8.11.3", "ulid": "^2.3.0" } } diff --git a/packages/core/src/analytic.ts b/packages/core/src/analytic.ts deleted file mode 100644 index febb01c..0000000 --- a/packages/core/src/analytic.ts +++ /dev/null @@ -1,88 +0,0 @@ -export * as Analytic from './analytic'; - -import { ulid } from 'ulid'; -import { SQL } from './sql'; -import { RawBuilder, sql } from 'kysely'; - -function json(obj: T): RawBuilder { - return sql`${obj}::jsonb`; -} - -export async function create( - name: string, - visitor_id: string, - url: string, - browser_name: string, - browser_version: string, - browser_engine: string, - device_model: string, - device_type: string, - device_vendor: string, - os_name: string, - os_version: string, - metadata: string -) { - const [result] = await SQL.DB.insertInto('analytic') - .values({ - id: ulid(), - visitor_id, - name, - url, - browser_name, - browser_version, - browser_engine, - device_model, - device_type, - device_vendor, - os_name, - os_version, - metadata: json(metadata), - }) - .returningAll() - .execute(); - - return result; -} - -export function getByEventName(name: string) { - return SQL.DB.selectFrom('analytic') - .selectAll() - .where('name', '=', name) - .orderBy('created', 'desc') - .execute(); -} - -export function list( - whereFields?: FieldQuery[] - // selectFields?: AnalyticsEventField[] -) { - let query = SQL.DB.selectFrom('analytic').selectAll(); - - // if (selectFields) { - // query = query.select(selectFields); - // } else { - // query = query.selectAll(); - // } - - whereFields?.forEach((field) => { - console.log(`inside list function`, field); - query = query.where(field.name, field.matcher || '=', field.value); - }); - - return query.orderBy('created', 'desc').execute(); -} - -export function countEvents() { - return SQL.DB.selectFrom('analytic') - .select(['name', (eb) => eb.fn.count('name').as('total')]) - .groupBy('name') - .execute(); -} - -export function getPageViews() { - return SQL.DB.selectFrom('analytic') - .select(['name', 'created', 'metadata']) - .where('name', '=', 'pageView') - .orderBy('created', 'desc') - .execute(); -} diff --git a/packages/core/src/post-dynamo.ts b/packages/core/src/post-dynamo.ts deleted file mode 100644 index 5ebd0d7..0000000 --- a/packages/core/src/post-dynamo.ts +++ /dev/null @@ -1,292 +0,0 @@ -export * as Post from './post-dynamo'; - -import { ulid } from 'ulid'; -import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; -import { - DeleteCommand, - DynamoDBDocumentClient, - GetCommand, - PutCommand, - ScanCommand, - QueryCommand, - UpdateCommand, -} from '@aws-sdk/lib-dynamodb'; - -export interface Post { - id: string; - title: string; - slug: string; - abstract: string; - content: string; - views: number; - likes: number; - tags: string[]; - isPublished: 1 | 0; - created: string; - publishedOn?: string; - updated?: string; -} - -export type PublishedPost = Post & { isPublished: 1; publishedOn: string }; - -const client = new DynamoDBClient(); -const docClient = DynamoDBDocumentClient.from(client); - -export type PostToCreate = Pick< - Post, - 'title' | 'slug' | 'abstract' | 'content' | 'tags' | 'publishedOn' | 'updated' ->; - -export async function create( - tableName: string, - post: PostToCreate & { isPublished?: 1 | 0; views?: number; likes?: number }, -) { - const id = ulid(); - const command = new PutCommand({ - TableName: tableName, - Item: { - id, - ...post, - isPublished: post.isPublished || 0, - views: post.views || 0, - likes: post.likes || 0, - created: new Date().toISOString(), - }, - }); - - console.log(`Post - create`, command.input); - - try { - await docClient.send(command); - console.log(`finished putting a Post...`); - } catch (err) { - console.log(`caught an error while creating a Post`, err); - } - - const created = await getById(tableName, id); - console.log(`created post`, created); - - return created; -} - -export async function getById(tableName: string, id: string) { - const command = new GetCommand({ - TableName: tableName, - Key: { - id, - }, - }); - - console.log(`Post - getById - start`, command.input); - - try { - const response = await docClient.send(command); - console.log(`Post - getById - response`, response); - return response['Item']; - } catch (err) { - console.log(`caught an error while getting a Post`, err); - return; - } -} - -export async function getBySlug(tableName: string, slug: string) { - const command = new QueryCommand({ - TableName: tableName, - IndexName: 'SlugIndex', - KeyConditionExpression: 'slug = :slug', - ExpressionAttributeValues: { - ':slug': slug, - }, - }); - - console.log(`Post - getBySlug - start`, slug); - - try { - const response = await docClient.send(command); - const result = response['Items'] || []; - console.log(`Post - getBySlug - response`, response.Count); - - return result[0] as Post | undefined; - } catch (err) { - console.log(`caught an error while getting a Post`, err); - return; - } -} - -export async function getAllBySlugs(tableName: string, slugs: string[]) { - const promises: Promise[] = []; - - console.log(`Post - getAllBySlugs`, slugs); - - for (const slug of slugs) { - promises.push(getBySlug(tableName, slug)); - } - - const result = await Promise.all(promises); - - console.log(`Post - getAllBySlugs - result`, result); - - return result.filter((result) => !!result) as Post[]; - // const slugFilter = slugs.map((_slug, index) => { - // return `:slug${index}`; - // }); - // const slugExpression = slugFilter.reduce((acc, curr, index) => { - // return { - // ...acc, - // [curr]: slugs[index], - // }; - // }, {}); - // const command = new ScanCommand({ - // TableName: tableName, - // IndexName: 'SlugIndex', - // FilterExpression: `slug IN (${slugFilter.join(', ')})`, - // ExpressionAttributeValues: slugExpression, - // }); - - // try { - // const response = await docClient.send(command); - // console.log(`Post - getAllBySlugs - response`, response); - // return response['Items'] as Post[]; - // } catch (err) { - // console.log(`caught an error while getting Posts by slugs`, err); - // return; - // } -} - -export async function list(tableName: string) { - const command = new ScanCommand({ - TableName: tableName, - }); - const result = await docClient.send(command); - - return result.Items || []; -} - -export async function queryPublished(tableName: string, tag?: string) { - const command = new QueryCommand({ - TableName: tableName, - IndexName: 'IsPublishedIndex', - KeyConditionExpression: 'isPublished = :val', - ExpressionAttributeValues: { - ':val': 1, - }, - }); - - const result = await docClient.send(command); - console.log(`queryPublished`, result.Count); - - let posts = (result.Items || []) as PublishedPost[]; - - if (tag) { - posts = posts.filter((post) => post.tags.includes(tag)); - console.log(`filtered posts by tag ${tag}...`); - } - - posts.sort( - (a, b) => - new Date(b.publishedOn).getTime() - new Date(a.publishedOn).getTime(), - ); - - return posts; -} - -export async function update(tableName: string, values: Partial) { - const { id, ...props } = values; - delete props.updated; - console.log(`Post - Update`); - - const setStatement = Object.keys(props) - .map((key) => { - console.log(key); - if (key === 'views') { - return `#views = :views`; - } - return `${key} = :${key}`; - }) - .join(', '); - const UpdateExpression = `set ${setStatement}, updated = :updated`; - console.log(`UpdateExpression`, UpdateExpression); - - const ExpressionAttributeValues: Record = Object.keys( - props, - ).reduce((acc, curr) => { - return { - ...acc, - [`:${curr}`]: values[curr as keyof Post], - }; - }, {}); - ExpressionAttributeValues.updated = new Date().toISOString(); - console.log(`ExpressionAttributeValues`, ExpressionAttributeValues); - - const command = new UpdateCommand({ - TableName: tableName, - Key: { - id, - }, - UpdateExpression, - ExpressionAttributeNames: { - '#views': 'views', - }, - ExpressionAttributeValues, - ReturnValues: 'ALL_NEW', - }); - - try { - const response = await docClient.send(command); - console.log(`Post - update - response`, response); - return response; - } catch (err) { - console.log(`caught an error while updating a Post`, err); - return; - } -} - -export async function increment( - tableName: string, - id: string, - attribute: 'views' | 'likes', -) { - const command = new UpdateCommand({ - TableName: tableName, - Key: { - id, - }, - UpdateExpression: 'ADD #cnt :val', - ExpressionAttributeNames: { '#cnt': attribute }, - ExpressionAttributeValues: { - ':val': 1, - }, - ReturnValues: 'UPDATED_NEW', - }); - const response = await docClient.send(command); - - console.log(`increment`, response); - - return response?.Attributes?.[attribute] as number; -} - -export async function deleteById(tableName: string, id: string) { - const deleteCommand = new DeleteCommand({ - TableName: tableName, - Key: { - id, - }, - }); - - return await docClient.send(deleteCommand); -} - -export async function deleteAll(tableName: string) { - const posts = await list(tableName); - console.log(`Post - deleteAll listed posts`, posts); - - for (const post of posts) { - const deleteCommand = new DeleteCommand({ - TableName: tableName, - Key: { - id: post.id, - }, - }); - await docClient.send(deleteCommand); - } -} diff --git a/packages/core/src/post.ts b/packages/core/src/post.ts index 337c208..b6a4a52 100644 --- a/packages/core/src/post.ts +++ b/packages/core/src/post.ts @@ -1,7 +1,16 @@ export * as Post from './post'; import { ulid } from 'ulid'; -import { SQL } from './sql'; +import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; +import { + DeleteCommand, + DynamoDBDocumentClient, + GetCommand, + PutCommand, + ScanCommand, + QueryCommand, + UpdateCommand, +} from '@aws-sdk/lib-dynamodb'; export interface Post { id: string; @@ -10,165 +19,274 @@ export interface Post { abstract: string; content: string; views: number; - is_published: boolean | null; - published_on: string | null; - created: Date; - updated: Date | null; + likes: number; + tags: string[]; + isPublished: 1 | 0; + created: string; + publishedOn?: string; + updated?: string; } -export type PostWithTags = Pick< - Post, - 'id' | 'title' | 'slug' | 'abstract' | 'content' | 'published_on' | 'updated' -> & { tags: string[] }; +export type PublishedPost = Post & { isPublished: 1; publishedOn: string }; -export type PublishedPostWithTags = PostWithTags & { published_on: string }; +const client = new DynamoDBClient(); +const docClient = DynamoDBDocumentClient.from(client); export type PostToCreate = Pick< Post, - 'title' | 'slug' | 'abstract' | 'content' | 'is_published' | 'published_on' + 'title' | 'slug' | 'abstract' | 'content' | 'tags' | 'publishedOn' | 'updated' >; -export async function create(post: PostToCreate) { - const result = await SQL.DB.insertInto('post') - .values({ - id: ulid(), +export async function create( + tableName: string, + post: PostToCreate & { isPublished?: 1 | 0; views?: number; likes?: number }, +) { + const id = ulid(); + const command = new PutCommand({ + TableName: tableName, + Item: { + id, ...post, - }) - .returningAll() - .executeTakeFirstOrThrow(); + isPublished: post.isPublished || 0, + views: post.views || 0, + likes: post.likes || 0, + created: new Date().toISOString(), + }, + }); - return result; -} + console.log(`Post - create`, command.input); -export async function createAll(posts: PostToCreate[]) { - const values = posts.map((post) => ({ - id: ulid(), - ...post, - })); + try { + await docClient.send(command); + console.log(`finished putting a Post...`); + } catch (err) { + console.log(`caught an error while creating a Post`, err); + } - const result = await SQL.DB.insertInto('post') - .values(values) - .returningAll() - .execute(); + const created = await getById(tableName, id); + console.log(`created post`, created); - return result; + return created; } -export async function updateAll(posts: Post[]) { - const queries = posts.map( - async ({ id, content, abstract, is_published, published_on }) => { - const result = await SQL.DB.updateTable('post') - .set({ - content, - abstract, - is_published, - published_on, - }) - .where('id', '=', id) - .executeTakeFirstOrThrow(); - - return result; +export async function getById(tableName: string, id: string) { + const command = new GetCommand({ + TableName: tableName, + Key: { + id, }, - ); + }); + + console.log(`Post - getById - start`, command.input); - const result = await Promise.all(queries); - return result; + try { + const response = await docClient.send(command); + console.log(`Post - getById - response`, response); + return response['Item']; + } catch (err) { + console.log(`caught an error while getting a Post`, err); + return; + } } -export async function getById(id: string) { - const result = await SQL.DB.selectFrom('post') - .selectAll() - .where('id', '=', id) - .executeTakeFirstOrThrow(); +export async function getBySlug(tableName: string, slug: string) { + const command = new QueryCommand({ + TableName: tableName, + IndexName: 'SlugIndex', + KeyConditionExpression: 'slug = :slug', + ExpressionAttributeValues: { + ':slug': slug, + }, + }); - return result; -} + console.log(`Post - getBySlug - start`, slug); -export async function getBySlug(slug: string) { - const result = await SQL.DB.selectFrom('post') - .selectAll() - .where('slug', '=', slug) - .executeTakeFirstOrThrow(); + try { + const response = await docClient.send(command); + const result = response['Items'] || []; + console.log(`Post - getBySlug - response`, response.Count); - return result; + return result[0] as Post | undefined; + } catch (err) { + console.log(`caught an error while getting a Post`, err); + return; + } } -export async function getAllBySlugs(slugs: string[]) { - const result = await SQL.DB.selectFrom('post') - .selectAll() - .where('slug', 'in', slugs) - .execute(); +export async function getAllBySlugs(tableName: string, slugs: string[]) { + const promises: Promise[] = []; + + console.log(`Post - getAllBySlugs`, slugs); + + for (const slug of slugs) { + promises.push(getBySlug(tableName, slug)); + } + + const result = await Promise.all(promises); - return result; + console.log(`Post - getAllBySlugs - result`, result); + + return result.filter((result) => !!result) as Post[]; + // const slugFilter = slugs.map((_slug, index) => { + // return `:slug${index}`; + // }); + // const slugExpression = slugFilter.reduce((acc, curr, index) => { + // return { + // ...acc, + // [curr]: slugs[index], + // }; + // }, {}); + // const command = new ScanCommand({ + // TableName: tableName, + // IndexName: 'SlugIndex', + // FilterExpression: `slug IN (${slugFilter.join(', ')})`, + // ExpressionAttributeValues: slugExpression, + // }); + + // try { + // const response = await docClient.send(command); + // console.log(`Post - getAllBySlugs - response`, response); + // return response['Items'] as Post[]; + // } catch (err) { + // console.log(`caught an error while getting Posts by slugs`, err); + // return; + // } } -export async function incrementViews(postId: string) { - const result = await SQL.DB.updateTable('post') - .set((eb) => ({ - views: eb('views', '+', 1), - })) - .where('id', '=', postId) - .returning('views') - .executeTakeFirstOrThrow(); +export async function list(tableName: string) { + const command = new ScanCommand({ + TableName: tableName, + }); + const result = await docClient.send(command); - return result; + return result.Items || []; } -export async function deleteById(id: string) { - const result = await SQL.DB.deleteFrom('post') - .where('id', '=', id) - .returning('id') - .execute(); +export async function queryPublished(tableName: string, tag?: string) { + const command = new QueryCommand({ + TableName: tableName, + IndexName: 'IsPublishedIndex', + KeyConditionExpression: 'isPublished = :val', + ExpressionAttributeValues: { + ':val': 1, + }, + }); + + const result = await docClient.send(command); + console.log(`queryPublished`, result.Count); + + let posts = (result.Items || []) as PublishedPost[]; - return result; + if (tag) { + posts = posts.filter((post) => post.tags.includes(tag)); + console.log(`filtered posts by tag ${tag}...`); + } + + posts.sort( + (a, b) => + new Date(b.publishedOn).getTime() - new Date(a.publishedOn).getTime(), + ); + + return posts; } -// @todo re-enable getting published posts only when we have enough published ones -export async function getPublishedPostsWithTags() { - const map: Record = {}; - const result = await SQL.DB.selectFrom('post') - .innerJoin('post_tag', 'post_tag.post_id', 'post.id') - .innerJoin('tag', 'post_tag.tag_id', 'tag.id') - .where('is_published', 'is not', null) - .select([ - 'post.id', - 'post.title', - 'post.slug', - 'post.abstract', - 'post.content', - 'post.published_on', - 'post.updated', - 'tag.name as tagName', - ]) - .orderBy('published_on', 'desc') - .execute(); - - // Since joining will give us multiple records for each post-tag relation, we need to reduce the results - for (const post of result) { - if (!map[post.id]) { - const { tagName, ...rest } = post; - map[post.id] = { - ...rest, - tags: [tagName], - } as PublishedPostWithTags; - } else { - map[post.id].tags.push(post.tagName); - } +export async function update(tableName: string, values: Partial) { + const { id, ...props } = values; + delete props.updated; + console.log(`Post - Update`); + + const setStatement = Object.keys(props) + .map((key) => { + console.log(key); + if (key === 'views') { + return `#views = :views`; + } + return `${key} = :${key}`; + }) + .join(', '); + const UpdateExpression = `set ${setStatement}, updated = :updated`; + console.log(`UpdateExpression`, UpdateExpression); + + const ExpressionAttributeValues: Record = Object.keys( + props, + ).reduce((acc, curr) => { + return { + ...acc, + [`:${curr}`]: values[curr as keyof Post], + }; + }, {}); + ExpressionAttributeValues.updated = new Date().toISOString(); + console.log(`ExpressionAttributeValues`, ExpressionAttributeValues); + + const command = new UpdateCommand({ + TableName: tableName, + Key: { + id, + }, + UpdateExpression, + ExpressionAttributeNames: { + '#views': 'views', + }, + ExpressionAttributeValues, + ReturnValues: 'ALL_NEW', + }); + + try { + const response = await docClient.send(command); + console.log(`Post - update - response`, response); + return response; + } catch (err) { + console.log(`caught an error while updating a Post`, err); + return; } +} + +export async function increment( + tableName: string, + id: string, + attribute: 'views' | 'likes', +) { + const command = new UpdateCommand({ + TableName: tableName, + Key: { + id, + }, + UpdateExpression: 'ADD #cnt :val', + ExpressionAttributeNames: { '#cnt': attribute }, + ExpressionAttributeValues: { + ':val': 1, + }, + ReturnValues: 'UPDATED_NEW', + }); + const response = await docClient.send(command); - return Object.values(map); + console.log(`increment`, response); + + return response?.Attributes?.[attribute] as number; } -export async function getPublishedPostsByTagWithRelations(tag: string) { - const postsWithTags = await getPublishedPostsWithTags(); +export async function deleteById(tableName: string, id: string) { + const deleteCommand = new DeleteCommand({ + TableName: tableName, + Key: { + id, + }, + }); - // Since we actually want all tag relations, we can't just use .where('tag.name', '=', tag) in the query - const result = postsWithTags.reduce((acc: PublishedPostWithTags[], curr) => { - if (curr.tags.includes(tag)) { - acc.push(curr); - } - return acc; - }, []); + return await docClient.send(deleteCommand); +} - return result; +export async function deleteAll(tableName: string) { + const posts = await list(tableName); + console.log(`Post - deleteAll listed posts`, posts); + + for (const post of posts) { + const deleteCommand = new DeleteCommand({ + TableName: tableName, + Key: { + id: post.id, + }, + }); + await docClient.send(deleteCommand); + } } diff --git a/packages/core/src/postTag.ts b/packages/core/src/postTag.ts deleted file mode 100644 index d584862..0000000 --- a/packages/core/src/postTag.ts +++ /dev/null @@ -1,46 +0,0 @@ -export * as PostTag from './postTag'; - -import { ulid } from 'ulid'; -import { SQL } from './sql'; - -export async function create(post_id: string, tag_id: string) { - const result = await SQL.DB.insertInto('post_tag') - .values({ id: ulid(), post_id, tag_id }) - .returningAll() - .execute(); - - return result; -} - -export async function deleteByIds(ids: string[]) { - const queries = ids.map(async (id) => { - const result = await SQL.DB.deleteFrom('post_tag') - .where('id', '=', id) - .returning('id') - .execute(); - - return result; - }); - - const result = await Promise.all(queries); - return result; -} - -export async function getAllByPostId(post_id: string) { - const result = await SQL.DB.selectFrom('post_tag') - .selectAll() - .where('post_id', '=', post_id) - .execute(); - - return result; -} - -export async function getByIds(post_id: string, tag_id: string) { - const result = await SQL.DB.selectFrom('post_tag') - .selectAll() - .where('post_id', '=', post_id) - .where('tag_id', '=', tag_id) - .executeTakeFirst(); - - return result; -} diff --git a/packages/core/src/postUpvote.ts b/packages/core/src/postUpvote.ts deleted file mode 100644 index a1b2c7e..0000000 --- a/packages/core/src/postUpvote.ts +++ /dev/null @@ -1,95 +0,0 @@ -export * as PostUpvote from './postUpvote'; - -import { sql } from 'kysely'; -import { ulid } from 'ulid'; -import { SQL } from './sql'; - -export async function create(post_id: string, visitor_id: string) { - const result = await SQL.DB.insertInto('post_upvote') - .values({ - id: ulid(), - post_id, - visitor_id, - }) - .returning('votes') - .executeTakeFirst(); - - if (!result?.votes) { - throw new Error('Post upvote not found.'); - } - - return { votes: result.votes }; -} - -export async function update(post_id: string, visitor_id: string) { - const result = await SQL.DB.updateTable('post_upvote') - .set((eb) => ({ - votes: eb('votes', '+', 1), - })) - .where('post_id', '=', post_id) - .where('visitor_id', '=', visitor_id) - .returning('votes') - .executeTakeFirst(); - - if (!result?.votes) { - throw new Error('Post upvote not found.'); - } - - return result; -} - -export async function getTotalPostUpvotes(post_id: string) { - const totalUpvotes = await SQL.DB.selectFrom('post_upvote') - .select('votes') - .where('post_upvote.post_id', '=', post_id) - .execute(); - - return { - votes: totalUpvotes.reduce((acc, curr) => acc + curr.votes, 0), - }; -} - -export function getRecentUpvotesByVisitorId( - post_id: string, - visitor_id: string, -) { - return SQL.DB.selectFrom('post_upvote') - .select(['id', 'votes']) - .where('post_upvote.post_id', '=', post_id) - .where('post_upvote.visitor_id', '=', visitor_id) - .where( - 'post_upvote.created', - '>', - // @ts-expect-error The docs don't give an example of how to make TS accept raw sql and I can't find anything on the internet about it - sql`CURRENT_TIMESTAMP - INTERVAL '1 day'`, - ) - .executeTakeFirst(); -} - -export async function updateById(id: string) { - const result = await SQL.DB.updateTable('post_upvote') - .set((eb) => ({ - votes: eb('votes', '+', 1), - })) - .where('id', '=', id) - .returning(['id', 'votes']) - .executeTakeFirst(); - - if (!result) { - throw new Error('Post upvote not found.'); - } - - return result; -} - -export function createOrUpdate( - post_id: string, - visitor_id: string, - upvote_id: string | undefined, -) { - if (upvote_id) { - return updateById(upvote_id); - } - - return create(post_id, visitor_id); -} diff --git a/packages/core/src/sql.generated.ts b/packages/core/src/sql.generated.ts deleted file mode 100644 index 3a7f9c9..0000000 --- a/packages/core/src/sql.generated.ts +++ /dev/null @@ -1,86 +0,0 @@ -import type { ColumnType } from "kysely"; - -export type Generated = T extends ColumnType - ? ColumnType - : ColumnType; - -export type Json = ColumnType; - -export type JsonArray = JsonValue[]; - -export type JsonObject = { - [K in string]?: JsonValue; -}; - -export type JsonPrimitive = boolean | null | number | string; - -export type JsonValue = JsonArray | JsonObject | JsonPrimitive; - -export type Timestamp = ColumnType; - -export interface Analytic { - id: string; - visitor_id: string; - name: string; - url: string; - browser_name: string; - browser_version: string; - browser_engine: string; - device_model: string; - device_type: string; - device_vendor: string; - os_name: string; - os_version: string; - metadata: Json; - created: Generated; -} - -export interface Post { - id: string; - title: string; - slug: string; - abstract: string; - content: string; - views: Generated; - is_published: boolean | null; - published_on: string | null; - created: Generated; - updated: Timestamp | null; -} - -export interface PostTag { - id: string; - post_id: string; - tag_id: string; - created: Generated; -} - -export interface PostUpvote { - id: string; - post_id: string; - visitor_id: string; - votes: Generated; - created: Generated; -} - -export interface Tag { - id: string; - name: string; - created: Generated; -} - -export interface User { - id: string; - email: string; - password: string; - created: Generated; -} - -export interface Database { - analytic: Analytic; - post: Post; - post_tag: PostTag; - post_upvote: PostUpvote; - tag: Tag; - user: User; -} diff --git a/packages/core/src/sql.ts b/packages/core/src/sql.ts deleted file mode 100644 index f420c8f..0000000 --- a/packages/core/src/sql.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { RDSData } from '@aws-sdk/client-rds-data'; -import { RDS } from 'sst/node/rds'; -import { Kysely, Selectable } from 'kysely'; -import { DataApiDialect } from 'kysely-data-api'; -import type { Database } from './sql.generated'; - -export const DB = new Kysely({ - dialect: new DataApiDialect({ - mode: 'postgres', - driver: { - secretArn: RDS.db.secretArn, - resourceArn: RDS.db.clusterArn, - database: RDS.db.defaultDatabaseName, - client: new RDSData({}), - }, - }), -}); - -export type Row = { - [Key in keyof Database]: Selectable; -}; - -export * as SQL from './sql'; diff --git a/packages/core/src/tag-dynamo.ts b/packages/core/src/tag-dynamo.ts deleted file mode 100644 index 55eee3f..0000000 --- a/packages/core/src/tag-dynamo.ts +++ /dev/null @@ -1,110 +0,0 @@ -export * as Tag from './tag-dynamo'; - -import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; -import { - BatchGetCommand, - DeleteCommand, - DynamoDBDocumentClient, - GetCommand, - PutCommand, - ScanCommand, -} from '@aws-sdk/lib-dynamodb'; - -export interface Tag { - name: string; - created: string; -} - -const client = new DynamoDBClient(); -const docClient = DynamoDBDocumentClient.from(client); - -export async function create(tableName: string, name: string) { - const command = new PutCommand({ - TableName: tableName, - Item: { - name, - created: new Date().toISOString(), - }, - }); - - console.log(`Tag - create`, command.input); - - try { - await docClient.send(command); - console.log(`finished putting a Tag...`); - } catch (err) { - console.log(`caught an error while creating a Tag`, err); - } - - const created = await getByName(tableName, name); - - return created; -} - -export async function getByName(tableName: string, name: string) { - const command = new GetCommand({ - TableName: tableName, - Key: { - name, - }, - }); - - console.log(`Tag - getByName - start`, command.input); - - try { - const response = await docClient.send(command); - console.log(`Tag - getByName - response`, response); - return response['Item']; - } catch (err) { - console.log(`caught an error while getting a Tag`, err); - return; - } -} - -export async function getAllByNames(tableName: string, names: string[]) { - const Keys = names.map((name) => ({ name })); - console.log(`Tag - getAllByNames`, Keys); - - const command = new BatchGetCommand({ - RequestItems: { - [tableName]: { - Keys, - }, - }, - }); - - try { - const result = await docClient.send(command); - const responses = result['Responses'] || {}; - console.log(`Tag - getAllByNames - response`, responses); - - return responses[tableName]; - } catch (err) { - console.log(`caught an error while batchGetting Tags`, err); - return; - } -} - -export async function list(tableName: string) { - const command = new ScanCommand({ - TableName: tableName, - }); - const result = await docClient.send(command); - - return result.Items || []; -} - -export async function deleteAll(tableName: string) { - const tags = await list(tableName); - console.log(`Tag - deleteAll listed Tags`, tags); - - for (const tag of tags) { - const deleteCommand = new DeleteCommand({ - TableName: tableName, - Key: { - name: tag.name, - }, - }); - await docClient.send(deleteCommand); - } -} diff --git a/packages/core/src/tag.ts b/packages/core/src/tag.ts index 497088a..c44d4d2 100644 --- a/packages/core/src/tag.ts +++ b/packages/core/src/tag.ts @@ -1,67 +1,110 @@ export * as Tag from './tag'; -import { ulid } from 'ulid'; -import { SQL } from './sql'; +import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; +import { + BatchGetCommand, + DeleteCommand, + DynamoDBDocumentClient, + GetCommand, + PutCommand, + ScanCommand, +} from '@aws-sdk/lib-dynamodb'; export interface Tag { - id: string; name: string; + created: string; } -export async function create(name: string) { - const result = await SQL.DB.insertInto('tag') - .values({ id: ulid(), name }) - .returningAll() - .executeTakeFirstOrThrow(); +const client = new DynamoDBClient(); +const docClient = DynamoDBDocumentClient.from(client); - return result; -} +export async function create(tableName: string, name: string) { + const command = new PutCommand({ + TableName: tableName, + Item: { + name, + created: new Date().toISOString(), + }, + }); + + console.log(`Tag - create`, command.input); -export async function createAll(names: string[]) { - const values = names.map((name) => ({ - id: ulid(), - name, - })); + try { + await docClient.send(command); + console.log(`finished putting a Tag...`); + } catch (err) { + console.log(`caught an error while creating a Tag`, err); + } - const result = await SQL.DB.insertInto('tag') - .values(values) - .returningAll() - .execute(); + const created = await getByName(tableName, name); - return result; + return created; } -export function list() { - return SQL.DB.selectFrom('tag') - .selectAll() - .orderBy('created', 'desc') - .execute(); +export async function getByName(tableName: string, name: string) { + const command = new GetCommand({ + TableName: tableName, + Key: { + name, + }, + }); + + console.log(`Tag - getByName - start`, command.input); + + try { + const response = await docClient.send(command); + console.log(`Tag - getByName - response`, response); + return response['Item']; + } catch (err) { + console.log(`caught an error while getting a Tag`, err); + return; + } } -export async function getAllByPostId(post_id: string) { - const result = await SQL.DB.selectFrom('tag') - .innerJoin('post_tag', 'tag.id', 'post_tag.tag_id') - .where('post_tag.post_id', '=', post_id) - .select(['tag.id', 'tag.name', 'post_tag.id as post_tag_id']) - .execute(); +export async function getAllByNames(tableName: string, names: string[]) { + const Keys = names.map((name) => ({ name })); + console.log(`Tag - getAllByNames`, Keys); + + const command = new BatchGetCommand({ + RequestItems: { + [tableName]: { + Keys, + }, + }, + }); + + try { + const result = await docClient.send(command); + const responses = result['Responses'] || {}; + console.log(`Tag - getAllByNames - response`, responses); - return result; + return responses[tableName]; + } catch (err) { + console.log(`caught an error while batchGetting Tags`, err); + return; + } } -export async function getByName(name: string) { - const result = await SQL.DB.selectFrom('tag') - .selectAll() - .where('name', '=', name) - .executeTakeFirst(); +export async function list(tableName: string) { + const command = new ScanCommand({ + TableName: tableName, + }); + const result = await docClient.send(command); - return result; + return result.Items || []; } -export async function getAllByNames(names: string[]) { - const result = await SQL.DB.selectFrom('tag') - .selectAll() - .where('name', 'in', names) - .execute(); +export async function deleteAll(tableName: string) { + const tags = await list(tableName); + console.log(`Tag - deleteAll listed Tags`, tags); - return result; + for (const tag of tags) { + const deleteCommand = new DeleteCommand({ + TableName: tableName, + Key: { + name: tag.name, + }, + }); + await docClient.send(deleteCommand); + } } diff --git a/packages/core/src/user.ts b/packages/core/src/user.ts deleted file mode 100644 index f985cea..0000000 --- a/packages/core/src/user.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * as User from './user'; - -import { SQL } from './sql'; - -export function getByEmail(email: string) { - return SQL.DB.selectFrom('user') - .selectAll() - .where('email', '=', email) - .executeTakeFirst(); -} diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts deleted file mode 100644 index d745077..0000000 --- a/packages/core/src/utils.ts +++ /dev/null @@ -1,36 +0,0 @@ -export * as dbUtils from './utils'; - -import { SQL } from './sql'; - -const tableNames = ['post', 'tag', 'post_tag'] as const; -export type TableName = (typeof tableNames)[number]; - -export async function listTableRecords(name: T) { - const result = await SQL.DB.selectFrom(name).selectAll().execute(); - - return result; -} - -export function deleteTableRecords(input: TableName | TableName[]) { - if (Array.isArray(input)) { - const queries = tableNames.map(async (name) => { - const result = await SQL.DB.deleteFrom(name).execute(); - - return result; - }); - - return Promise.all(queries); - } - - return SQL.DB.deleteFrom(input).execute(); -} - -/** - * Postgres strips timezone info from dates and adds a space where the T should go - * @param timestamp example 2024-01-29 00:00:00 instead of 2024-01-29T00:00:00.000Z - * @returns the ISO formatted string - */ -export function convertDbTimestampToISOString(timestamp: string) { - const result = timestamp.replace(' ', 'T'); - return result + 'Z'; -} diff --git a/packages/functions/package.json b/packages/functions/package.json index 974ea54..00212dd 100644 --- a/packages/functions/package.json +++ b/packages/functions/package.json @@ -7,19 +7,9 @@ "typecheck": "tsc -noEmit" }, "devDependencies": { - "@types/aws-lambda": "^8.10.136", - "@types/bcryptjs": "^2.4.6", - "@types/jsonwebtoken": "^9.0.6", "@types/node": "^20.11.26" }, "dependencies": { - "bcryptjs": "^2.4.3", - "cheerio": "1.0.0-rc.12", - "device-detector-js": "^3.0.3", - "gray-matter": "^4.0.3", - "jsonwebtoken": "^9.0.2", - "node-fetch": "^3.3.2", - "resend": "^3.2.0", - "ulid": "^2.3.0" + "gray-matter": "^4.0.3" } } diff --git a/packages/functions/src/seed.test.ts b/packages/functions/src/seed.test.ts index 0f8e165..e695a78 100644 --- a/packages/functions/src/seed.test.ts +++ b/packages/functions/src/seed.test.ts @@ -1,7 +1,7 @@ -import { Post } from '@core/post-dynamo'; +import { Post } from '@core/post'; import { afterAll, beforeAll, expect, it, describe } from 'vitest'; import { onCreate, onUpdate } from './seed'; -import { Tag } from '@core/tag-dynamo'; +import { Tag } from '@core/tag'; import { Table } from 'sst/node/table'; const PostTable = Table.Post.tableName; diff --git a/packages/functions/src/seed.ts b/packages/functions/src/seed.ts index 276a774..a1a0226 100644 --- a/packages/functions/src/seed.ts +++ b/packages/functions/src/seed.ts @@ -1,5 +1,5 @@ -import { Post, PostToCreate } from '@core/post-dynamo'; -import { Tag } from '@core/tag-dynamo'; +import { Post, PostToCreate } from '@core/post'; +import { Tag } from '@core/tag'; import { BlogPost, getBlogPosts } from './utils/file-helpers'; import { Table } from 'sst/node/table'; diff --git a/packages/web/.eslintrc.json b/packages/web/.eslintrc.json index 688866e..05aacb9 100644 --- a/packages/web/.eslintrc.json +++ b/packages/web/.eslintrc.json @@ -1,4 +1,4 @@ { - "extends": ["next/babel", "next/core-web-vitals", "prettier"], + "extends": ["next/core-web-vitals", "prettier"], "rules": { "react/no-unescaped-entities": 0 } } diff --git a/packages/web/actions/getPublishedPostsWithTagsAction.ts b/packages/web/actions/getPublishedPostsWithTagsAction.ts deleted file mode 100644 index 0f5492a..0000000 --- a/packages/web/actions/getPublishedPostsWithTagsAction.ts +++ /dev/null @@ -1,22 +0,0 @@ -'use server'; - -import { Post } from '@core/post'; - -async function getPosts(tag?: string) { - let posts = []; - let query = Post.getPublishedPostsWithTags; - - if (tag) { - query = () => Post.getPublishedPostsByTagWithRelations(tag); - } - - try { - posts = await query(); - } catch (err) { - posts = await query(); - } - - return posts; -} - -export default getPosts; diff --git a/packages/web/actions/incrementUpvote.ts b/packages/web/actions/incrementUpvote.ts deleted file mode 100644 index de22070..0000000 --- a/packages/web/actions/incrementUpvote.ts +++ /dev/null @@ -1,16 +0,0 @@ -// Accessing anything bound in SST in a server action file like this is broken due to a top level await error -// For now, such server actions must be written inline in a server component -'use server'; - -import { PostUpvote } from '../../core/src/postUpvote'; -import { revalidatePath } from 'next/cache'; - -export async function incrementUpvote( - postId: string, - visitorId: string, - recentVote: string | undefined, -) { - await PostUpvote.createOrUpdate(postId, visitorId, recentVote); // Accessing bound DB functions causes an error, do not use until SST fixes it - - revalidatePath('/[postSlug]', 'page'); -} diff --git a/packages/web/app/blog/[postSlug]/page.test.tsx b/packages/web/app/blog/[postSlug]/page.test.tsx index d5ca220..165bcd4 100644 --- a/packages/web/app/blog/[postSlug]/page.test.tsx +++ b/packages/web/app/blog/[postSlug]/page.test.tsx @@ -1,11 +1,11 @@ -import { expect, it, vi } from 'vitest' +import { expect, it, vi } from 'vitest'; import { render, screen } from '@testing-library/react'; import { Post } from '@core/post'; -import { Tag } from '@core/tag'; -import Page from './page' +import Page from './page'; vi.mock('react', () => { - const testCache = ) => unknown>(func: T) => func; + const testCache = ) => unknown>(func: T) => + func; const originalModule = vi.importActual('react'); return { ...originalModule, @@ -14,48 +14,57 @@ vi.mock('react', () => { }); vi.mock('next-mdx-remote/rsc', () => { - const mockMDXRemote =
MDXRemote
; - const originalModule = vi.importActual('next-mdx-remote/rsc');; + const mockMDXRemote = ( +
+ MDXRemote +
+ ); + const originalModule = vi.importActual('next-mdx-remote/rsc'); return { ...originalModule, - MDXRemote: vi.fn().mockReturnValue(mockMDXRemote) - } + MDXRemote: vi.fn().mockReturnValue(mockMDXRemote), + }; }); vi.mock('@/utils/mdx-components', () => ({ default: { pre: vi.fn(), - Sandpack: vi.fn() - } + Sandpack: vi.fn(), + }, })); // async Server Components aren't supported yet with Vitest or Jest vi.mock('@/components/Upvotes', () => ({ - Upvotes: vi.fn() -})) + Upvotes: vi.fn(), +})); vi.mock('@/components/PageViews', () => ({ - default: vi.fn() -})) + default: vi.fn(), +})); +vi.mock('@/components/TableOfContents', () => ({ + default: vi.fn(), +})); it('renders a published post', async () => { - vi.spyOn(Post, 'getBySlug').mockImplementation(vi.fn().mockReturnValue(Promise.resolve({ - id: '1', - title: 'Published Post', - slug: 'published-post', - abstract: 'A published post', - content: '

Hello world!

Second Heading

', - views: 0, - published_on: new Date('2024-01-26 EST'), - is_published: true, - created: new Date('2024-01-26 EST'), - updated: null - }))); - vi.spyOn(Tag, 'getAllByPostId').mockImplementation(vi.fn().mockReturnValue(Promise.resolve([ - { id: '1', name: 'javascript', created: new Date('2024-01-26 EST') } - ])) + vi.spyOn(Post, 'getBySlug').mockImplementation( + vi.fn().mockReturnValue( + Promise.resolve({ + id: '1', + title: 'Published Post', + slug: 'published-post', + abstract: 'A published post', + content: '

Hello world!

Second Heading

', + views: 0, + likes: 0, + tags: ['javascript'], + publishedOn: new Date('2024-01-26 EST'), + isPublished: 1, + created: new Date('2024-01-26 EST'), + updated: null, + }), + ), ); // @ts-expect-error server components aren't built into RTL render function yet render(await Page({ params: { postSlug: 'test-post' } })); - expect(screen.getByRole('heading', { level: 2 })).toBeDefined() -}) \ No newline at end of file + expect(screen.getByRole('heading', { level: 1 })).toBeDefined(); +}); diff --git a/packages/web/app/blog/[postSlug]/page.tsx b/packages/web/app/blog/[postSlug]/page.tsx index e3fd449..17c8907 100644 --- a/packages/web/app/blog/[postSlug]/page.tsx +++ b/packages/web/app/blog/[postSlug]/page.tsx @@ -5,7 +5,7 @@ import COMPONENT_MAP from '@/utils/mdx-components'; import { cache } from 'react'; import { notFound } from 'next/navigation'; import { Upvotes } from '@/components/Upvotes'; -import { Post } from '@core/post-dynamo'; +import { Post } from '@core/post'; import PageViews from '@/components/PageViews'; import { marked } from 'marked'; import * as cheerio from 'cheerio'; @@ -14,7 +14,6 @@ import TableOfContents from '@/components/TableOfContents'; import SupportingLink from '@/components/SupportingLink'; import slugify from '@/utils/slugify'; import dayjs from '@/utils/extendedDayJs'; -import PostMetadata from '@/components/PostMetadata'; import { Table } from 'sst/node/table'; import LogRocket from 'logrocket'; @@ -77,7 +76,6 @@ const PostPage: NextPage<{ params: { postSlug: string } }> = async ({ id, title, slug, - abstract, publishedOn, content, views, @@ -92,7 +90,6 @@ const PostPage: NextPage<{ params: { postSlug: string } }> = async ({ return ( <> -

diff --git a/packages/web/components/PageViews/PageViews.tsx b/packages/web/components/PageViews/PageViews.tsx index 073254a..bcc4939 100644 --- a/packages/web/components/PageViews/PageViews.tsx +++ b/packages/web/components/PageViews/PageViews.tsx @@ -1,6 +1,6 @@ import range from '@/utils/range'; import styles from './PageViews.module.css'; -import { Post } from '@core/post-dynamo'; +import { Post } from '@core/post'; import { Table } from 'sst/node/table'; const PostTable = Table.Post.tableName; diff --git a/packages/web/components/PostGallery/PostGallery.tsx b/packages/web/components/PostGallery/PostGallery.tsx index 80218e0..8a298a9 100644 --- a/packages/web/components/PostGallery/PostGallery.tsx +++ b/packages/web/components/PostGallery/PostGallery.tsx @@ -1,7 +1,7 @@ 'use client'; import styles from './PostGallery.module.css'; -import { PublishedPost } from '@core/post-dynamo'; +import { PublishedPost } from '@core/post'; import PostCard from '@/components/PostCard'; import { useEffect, useState } from 'react'; import PostSkeletonGallery from '../PostSkeletonGallery'; @@ -28,7 +28,7 @@ const PostGallery = ({ getPosts, numSkeletonPosts }: Props) => { } catch (err) { LogRocket.captureException(toErrorWithMessage(err)); } - }, []); + }, [getPosts]); return (
diff --git a/packages/web/components/PostGalleryContainer/PostGalleryContainer.tsx b/packages/web/components/PostGalleryContainer/PostGalleryContainer.tsx index 608ab64..6f033d2 100644 --- a/packages/web/components/PostGalleryContainer/PostGalleryContainer.tsx +++ b/packages/web/components/PostGalleryContainer/PostGalleryContainer.tsx @@ -1,4 +1,4 @@ -import { Post } from '@core/post-dynamo'; +import { Post } from '@core/post'; import PostGallery from '../PostGallery'; import { Table } from 'sst/node/table'; @@ -13,7 +13,8 @@ const PostGalleryContainer = ({ tag, numSkeletonPosts = 4 }: Props) => { async function getPosts() { 'use server'; - return Post.queryPublished(PostTable, tag); + const posts = await Post.queryPublished(PostTable, tag); + return posts; } return ( diff --git a/packages/web/components/PostMetadata/PostMetadata.module.css b/packages/web/components/PostMetadata/PostMetadata.module.css deleted file mode 100644 index e69de29..0000000 diff --git a/packages/web/components/PostMetadata/PostMetadata.tsx b/packages/web/components/PostMetadata/PostMetadata.tsx deleted file mode 100644 index abd0bd9..0000000 --- a/packages/web/components/PostMetadata/PostMetadata.tsx +++ /dev/null @@ -1,28 +0,0 @@ -'use client'; - -import { useLayoutEffect } from 'react'; - -interface Props { - title: string; - description: string; -} - -const PostMetadata = ({ title, description }: Props) => { - useLayoutEffect(() => { - if (!!title && !!description) { - document.title = title; - const head = document.querySelector('head'); - - if (head) { - const meta = document.createElement('meta'); - meta.name = 'description'; - meta.content = description; - head.appendChild(meta); - } - } - }, [title, description]); - - return null; -}; - -export default PostMetadata; diff --git a/packages/web/components/PostMetadata/index.ts b/packages/web/components/PostMetadata/index.ts deleted file mode 100644 index c4fffc2..0000000 --- a/packages/web/components/PostMetadata/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './PostMetadata'; \ No newline at end of file diff --git a/packages/web/components/TagSidebar/TagSidebar.tsx b/packages/web/components/TagSidebar/TagSidebar.tsx index 23524b0..3661629 100644 --- a/packages/web/components/TagSidebar/TagSidebar.tsx +++ b/packages/web/components/TagSidebar/TagSidebar.tsx @@ -1,7 +1,7 @@ import PrimaryLink from '../PrimaryLink'; import TagSkeleton from '../TagSkeleton'; import styles from './TagSidebar.module.css'; -import { Tag } from '@core/tag-dynamo'; +import { Tag } from '@core/tag'; import { Table } from 'sst/node/table'; const TagTable = Table.Tag.tableName; diff --git a/packages/web/components/Upvotes/Upvotes.tsx b/packages/web/components/Upvotes/Upvotes.tsx index 9311079..ed76ce6 100644 --- a/packages/web/components/Upvotes/Upvotes.tsx +++ b/packages/web/components/Upvotes/Upvotes.tsx @@ -20,6 +20,8 @@ const Upvotes = ({ initialVotes, className, incrementVotes }: Props) => { const [optimisticVotes, setOptimisticVotes] = useState(initialVotes); const isMaxedOut = optimisticVotes === MAX_VOTES; + // This function doesn't need any dependencies + // eslint-disable-next-line react-hooks/exhaustive-deps const stableDebouncedHandleIncrementVotes = useCallback( debounce(async () => { await incrementVotes(); @@ -31,7 +33,7 @@ const Upvotes = ({ initialVotes, className, incrementVotes }: Props) => { return (