From dc3c4531f5dadbdd8ae1376ac05428a7be93e5e6 Mon Sep 17 00:00:00 2001 From: JG Date: Wed, 31 Jul 2024 15:50:48 +0800 Subject: [PATCH 1/7] blog: update supabase rls alternative --- blog/supabase-alternative/index.md | 45 +++++++++++++++--------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/blog/supabase-alternative/index.md b/blog/supabase-alternative/index.md index 1c4e014c..a560e81e 100644 --- a/blog/supabase-alternative/index.md +++ b/blog/supabase-alternative/index.md @@ -186,13 +186,26 @@ Everything looks perfect, especially if you are familiar with SQL. So why do I n ![perfect-meme](https://github.com/user-attachments/assets/08ac420c-4f2b-4147-b4b5-a6c766ea61a3) ## The Problems of Supabse RLS -### 1. **Separation from Application Logic** +### 1. Separation from Application Logic As a modern developer, you know the sense of control when you can launch with a one-click. The prerequisite is to keep everything within the database. It's not just about convenience; it's about maintaining a single source of truth, ensuring consistency, and streamlining your workflow. However, for RLS, you have to define authorization directly in the database, not in your source code. Of course, you could store it as an SQL file in your codebase, but you need to rely on SQL migration to ensure consistency. I think it’s the same reason why you seldom see people using stored procedures of databases nowadays despite all the benefits they offer. -What makes the consistency even worse is that, according to the official documentation of Supabase, you have to duplicate the policy filter in your application code for performance reasons: +What makes the consistency even worse is that you have to duplicate the policy filters in the application code. For example, if you are using the Supabase JS SDK, you have to use the two queries below to get the result: +```tsx +// First, get the user's spaceIds +const { data: userSpaces } = await supabase.from('SpaceUser').select('spaceId').eq('userId', userId); + +// Extract spaceIds from the result +const userSpaceIds = userSpaces.map((space) => space.spaceId); + +// Now, query the List table +const { data, error } = await supabase + .from('List') + .select('*') +``` +Why? Because otherwise, you might experience 20x slower query performance, according to the official benchmark of Supabase. 😲 [Add filters to every query | Supabase Docs](https://supabase.com/docs/guides/database/postgres/row-level-security#add-filters-to-every-query) @@ -318,38 +331,26 @@ What about the problems of RLS? Let’s go through them one by one. The access policies are now defined alongside the database models. The schema becomes the single source of truth of your backend, enabling an easier understanding of the system as a whole. -Moreover, whether calling from the frontend or backend, you don’t need to use any filter regarding the authorization rules, which will be injected into queries automatically by the ZenStack runtime. +Moreover, whether calling from the frontend or backend, you don’t need to use any filter regarding the authorization rules, which will be injected into queries automatically by the ZenStack runtime. The application code you need to write is clean and clear: ```tsx - // frontend query - const { data: lists } = useFindManyList( - { - where: { - space: { - slug: router.query.slug as string, - }, - }, - } - ); + // frontend query: + // ZenStack generated hooks only returns the data user is allowed to read + const { data: lists } = useFindManyList(); ... - // server props + // server props export const getServerSideProps: GetServerSideProps = async ({ req, res, params }) => { const db = await getEnhancedPrisma({ req, res }); - - const lists = await db.list.findMany({ - where: { - space: { slug: params?.slug as string }, - } - }); + // ZenStack enhanced Prisma client only returns the data user is allowed to read + const lists = await db.list.findMany(); return { props: { lists }, }; }; ``` -> The `where` clause above is to get the data from current space because one user could join multiple `Space`. It’s the application behavior instead of Authorization -> +> Remember the complex query you need to write for the RLS case mentioned above? ### 2. Good DX From bb8fc8d915085184fd09f44f3bcb7fcb0ed25645 Mon Sep 17 00:00:00 2001 From: JG Date: Sun, 1 Sep 2024 00:09:39 +0800 Subject: [PATCH 2/7] blog: add related posts for blog page --- docusaurus.config.js | 14 ++++-- src/components/blog/post-paginator.tsx | 64 ++++++++++++++++++++++++ src/plugins/blog-plugin.js | 69 ++++++++++++++++++++++++++ src/theme/BlogPostPage/index.js | 18 ++++++- 4 files changed, 158 insertions(+), 7 deletions(-) create mode 100644 src/components/blog/post-paginator.tsx create mode 100644 src/plugins/blog-plugin.js diff --git a/docusaurus.config.js b/docusaurus.config.js index 18087dec..4510626c 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -46,11 +46,7 @@ const config = { }, }, }, - blog: { - showReadingTime: true, - blogSidebarTitle: 'Recent posts', - blogSidebarCount: 10, - }, + blog: false, theme: { customCss: require.resolve('./src/css/custom.css'), }, @@ -260,6 +256,14 @@ const config = { }, }; }, + [ + './src/plugins/blog-plugin.js', + { + showReadingTime: true, + blogSidebarTitle: 'Recent posts', + blogSidebarCount: 10, + }, + ], ], markdown: { diff --git a/src/components/blog/post-paginator.tsx b/src/components/blog/post-paginator.tsx new file mode 100644 index 00000000..454ab70c --- /dev/null +++ b/src/components/blog/post-paginator.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import Link from '@docusaurus/Link'; + +import clsx from 'clsx'; + +export const PostPaginator = ({ posts, title }) => { + if (posts.length < 1) { + return null; + } + + return ( +
+
+

{title}

+
+ {posts.map((post) => ( + +
+ {post.title} +
+ +

{post.description}

+ + ))} +
+
+
+ ); +}; diff --git a/src/plugins/blog-plugin.js b/src/plugins/blog-plugin.js new file mode 100644 index 00000000..0953b34f --- /dev/null +++ b/src/plugins/blog-plugin.js @@ -0,0 +1,69 @@ +const blogPluginExports = require('@docusaurus/plugin-content-blog'); +const utils = require('@docusaurus/utils'); +const path = require('path'); + +const defaultBlogPlugin = blogPluginExports.default; + +function getRelatedPosts(allBlogPosts, metadata) { + const relatedPosts = allBlogPosts.filter( + (post) => + post.metadata.frontMatter.tags + ?.filter((tag) => tag?.toLowerCase() != 'zenstack') + .some((tag) => metadata.frontMatter.tags?.includes(tag)) && post.metadata.title !== metadata.title + ); + + const filteredPostInfos = relatedPosts.map((post) => { + return { + title: post.metadata.title, + description: post.metadata.description, + permalink: post.metadata.permalink, + formattedDate: post.metadata.formattedDate, + authors: post.metadata.authors, + readingTime: post.metadata.readingTime, + date: post.metadata.date, + }; + }); + + return filteredPostInfos; +} + +async function blogPluginExtended(...pluginArgs) { + const blogPluginInstance = await defaultBlogPlugin(...pluginArgs); + + return { + // Add all properties of the default blog plugin so existing functionality is preserved + ...blogPluginInstance, + contentLoaded: async function (data) { + await blogPluginInstance.contentLoaded(data); + const { content: blogContents, actions } = data; + const { blogPosts: allBlogPosts } = blogContents; + const { addRoute, createData } = actions; + // Create routes for blog entries. + await Promise.all( + allBlogPosts.map(async (blogPost) => { + const { id, metadata } = blogPost; + const relatedPosts = getRelatedPosts(allBlogPosts, metadata); + await createData( + // Note that this created data path must be in sync with + // metadataPath provided to mdx-loader. + `${utils.docuHash(metadata.source)}.json`, + JSON.stringify({ ...metadata, relatedPosts }, null, 2) + ); + addRoute({ + path: metadata.permalink, + component: '@theme/BlogPostPage', + exact: true, + modules: { + content: metadata.source, + }, + }); + }) + ); + }, + }; +} + +module.exports = { + ...blogPluginExports, + default: blogPluginExtended, +}; diff --git a/src/theme/BlogPostPage/index.js b/src/theme/BlogPostPage/index.js index 5dc96748..2805db62 100644 --- a/src/theme/BlogPostPage/index.js +++ b/src/theme/BlogPostPage/index.js @@ -9,17 +9,29 @@ import BlogPostPageMetadata from '@theme/BlogPostPage/Metadata'; import TOC from '@theme/TOC'; import Unlisted from '@theme/Unlisted'; import GiscusComponent from '@site/src/components/GiscusComponent'; -function BlogPostPageContent({ sidebar, children }) { +import { PostPaginator } from '@site/src/components/blog/post-paginator'; + +function getMultipleRandomElement(arr, num) { + const shuffled = [...arr].sort(() => 0.5 - Math.random()); + + return shuffled.slice(0, num); +} + +function BlogPostPageContent({ children }) { const { metadata, toc } = useBlogPost(); + const { relatedPosts } = metadata; + const { nextItem, prevItem, frontMatter, unlisted } = metadata; const { hide_table_of_contents: hideTableOfContents, toc_min_heading_level: tocMinHeadingLevel, toc_max_heading_level: tocMaxHeadingLevel, } = frontMatter; + + const randomThreeRelatedPosts = getMultipleRandomElement(relatedPosts, 3); + return ( 0 ? ( @@ -28,6 +40,7 @@ function BlogPostPageContent({ sidebar, children }) { > {unlisted && } {children} + {(nextItem || prevItem) && } @@ -35,6 +48,7 @@ function BlogPostPageContent({ sidebar, children }) { } export default function BlogPostPage(props) { const BlogPostContent = props.content; + console.log('props', props); return ( Date: Sun, 1 Sep 2024 00:11:39 +0800 Subject: [PATCH 3/7] remove console.log --- src/theme/BlogPostPage/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/theme/BlogPostPage/index.js b/src/theme/BlogPostPage/index.js index 2805db62..8ef412d3 100644 --- a/src/theme/BlogPostPage/index.js +++ b/src/theme/BlogPostPage/index.js @@ -48,7 +48,6 @@ function BlogPostPageContent({ children }) { } export default function BlogPostPage(props) { const BlogPostContent = props.content; - console.log('props', props); return ( Date: Sun, 1 Sep 2024 00:24:43 +0800 Subject: [PATCH 4/7] at least make 3 related posts --- src/plugins/blog-plugin.js | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/plugins/blog-plugin.js b/src/plugins/blog-plugin.js index 0953b34f..872e50fd 100644 --- a/src/plugins/blog-plugin.js +++ b/src/plugins/blog-plugin.js @@ -3,15 +3,31 @@ const utils = require('@docusaurus/utils'); const path = require('path'); const defaultBlogPlugin = blogPluginExports.default; +const MIN_RELATED_POSTS = 3; + +function getMultipleRandomElement(arr, num) { + const shuffled = [...arr].sort(() => 0.5 - Math.random()); + + return shuffled.slice(0, num); +} function getRelatedPosts(allBlogPosts, metadata) { - const relatedPosts = allBlogPosts.filter( + let relatedPosts = allBlogPosts.filter( (post) => post.metadata.frontMatter.tags ?.filter((tag) => tag?.toLowerCase() != 'zenstack') .some((tag) => metadata.frontMatter.tags?.includes(tag)) && post.metadata.title !== metadata.title ); + if (relatedPosts.length < MIN_RELATED_POSTS) { + remainingCount = MIN_RELATED_POSTS - relatedPosts.length; + const remainingPosts = getMultipleRandomElement( + allBlogPosts.filter((post) => !relatedPosts.includes(post)), + remainingCount + ); + relatedPosts = relatedPosts.concat(remainingPosts); + } + const filteredPostInfos = relatedPosts.map((post) => { return { title: post.metadata.title, @@ -49,14 +65,6 @@ async function blogPluginExtended(...pluginArgs) { `${utils.docuHash(metadata.source)}.json`, JSON.stringify({ ...metadata, relatedPosts }, null, 2) ); - addRoute({ - path: metadata.permalink, - component: '@theme/BlogPostPage', - exact: true, - modules: { - content: metadata.source, - }, - }); }) ); }, From 74b82e9f9aa80cfcf1e0d1dd8f2c855f714767e1 Mon Sep 17 00:00:00 2001 From: JG Date: Sun, 1 Sep 2024 00:28:33 +0800 Subject: [PATCH 5/7] update style --- src/components/blog/post-paginator.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/blog/post-paginator.tsx b/src/components/blog/post-paginator.tsx index 454ab70c..dc0d779c 100644 --- a/src/components/blog/post-paginator.tsx +++ b/src/components/blog/post-paginator.tsx @@ -21,7 +21,7 @@ export const PostPaginator = ({ posts, title }) => { )} >
-

{title}

+

{title}

{posts.map((post) => ( Date: Tue, 3 Sep 2024 12:48:13 +0800 Subject: [PATCH 6/7] add CTA to doc --- blog/good-dx/index.md | 2 +- blog/microservice/index.md | 2 +- blog/ocp/index.md | 2 +- blog/polymorphism/index.md | 2 +- blog/saas-backend/index.md | 2 +- blog/supabase-alternative/index.md | 2 +- src/components/blog/post-paginator.tsx | 17 ++++++----- src/plugins/blog-plugin.js | 15 +++++----- src/theme/BlogPostPage/index.js | 41 +++++++++++++++++++++++--- 9 files changed, 60 insertions(+), 25 deletions(-) diff --git a/blog/good-dx/index.md b/blog/good-dx/index.md index d5a036cc..a9578d21 100644 --- a/blog/good-dx/index.md +++ b/blog/good-dx/index.md @@ -1,7 +1,7 @@ --- title: 'How to make good DX(Developer Experience): Empathize' description: Why DX matters and how to create great DX toolkit for developers. -tags: [DX, zensatck] +tags: [DX, zenstack, programming] authors: jiasheng date: 2023-03-24 image: ./cover.png diff --git a/blog/microservice/index.md b/blog/microservice/index.md index ac252f2d..45bb90a8 100644 --- a/blog/microservice/index.md +++ b/blog/microservice/index.md @@ -1,7 +1,7 @@ --- title: Where Did Microservices Go description: Microservices have undergone a shift due to a combination of lessons learned and the emergence of new technologies. -tags: [zenstack, Microservices] +tags: [zenstack, microservices, fullstack, nextjs] authors: jiasheng date: 2023-04-22 image: ./cover.jpg diff --git a/blog/ocp/index.md b/blog/ocp/index.md index d16b8a1d..c1be131f 100644 --- a/blog/ocp/index.md +++ b/blog/ocp/index.md @@ -1,6 +1,6 @@ --- description: Use a real example to illustrate how to achieve good design by applying polymorphism from the database to the UI. -tags: [prisma, orm, database, oop, design, typescript] +tags: [orm, database, oop, design, typescript, polymorphism] authors: jiasheng image: ./cover.jpg date: 2024-03-28 diff --git a/blog/polymorphism/index.md b/blog/polymorphism/index.md index 516ded0c..82ef4c23 100644 --- a/blog/polymorphism/index.md +++ b/blog/polymorphism/index.md @@ -1,6 +1,6 @@ --- description: This post explores different patterns for implementing polymorphism in ORMs and how ZenStack can add the missing feature into Prisma. -tags: [prisma, orm, database] +tags: [prisma, orm, database, polymorphism] authors: yiming date: 2023-12-21 image: ./cover.png diff --git a/blog/saas-backend/index.md b/blog/saas-backend/index.md index 52bcb0be..2d6e18ab 100644 --- a/blog/saas-backend/index.md +++ b/blog/saas-backend/index.md @@ -1,7 +1,7 @@ --- title: 'How To Build a Scalable SaaS Backend in 10 Minutes With 100 Lines of Code' description: Use schema as the single source of truth for the SaaS backend -tags: [zenstack, saas, backend, access-control, nodejs, typescript] +tags: [zenstack, saas, backend, authorization, nodejs] authors: jiasheng date: 2023-06-21 image: ./cover.png diff --git a/blog/supabase-alternative/index.md b/blog/supabase-alternative/index.md index a03e9d7a..21d6a326 100644 --- a/blog/supabase-alternative/index.md +++ b/blog/supabase-alternative/index.md @@ -1,7 +1,7 @@ --- title: Supabase RLS Alternative description: Show the limitation of Supabase RLS(Row Level Security) with a multi-tenancy SaaS example and introduce ZenStack as an alternative. -tags: [supabase, rls, auth, baas, zenstack] +tags: [supabase, rls, auth, authorization, baas, zenstack] authors: jiasheng date: 2024-07-24 image: ./cover.png diff --git a/src/components/blog/post-paginator.tsx b/src/components/blog/post-paginator.tsx index dc0d779c..0468a7fb 100644 --- a/src/components/blog/post-paginator.tsx +++ b/src/components/blog/post-paginator.tsx @@ -22,12 +22,13 @@ export const PostPaginator = ({ posts, title }) => { >

{title}

-
+
{posts.map((post) => ( { 'group' )} > -
- {post.title} -
+
{post.title}

{post.description}

))} +

+ 🚀 Ready to build high-quality, scalable Prisma apps with built-in AuthZ and instant CRUD APIs ? +

+ + Get started with ZenStack's ultimate guide to build faster and smarter +
diff --git a/src/plugins/blog-plugin.js b/src/plugins/blog-plugin.js index 872e50fd..157cd842 100644 --- a/src/plugins/blog-plugin.js +++ b/src/plugins/blog-plugin.js @@ -3,7 +3,7 @@ const utils = require('@docusaurus/utils'); const path = require('path'); const defaultBlogPlugin = blogPluginExports.default; -const MIN_RELATED_POSTS = 3; +const MIN_RELATED_POSTS = 10; function getMultipleRandomElement(arr, num) { const shuffled = [...arr].sort(() => 0.5 - Math.random()); @@ -12,17 +12,17 @@ function getMultipleRandomElement(arr, num) { } function getRelatedPosts(allBlogPosts, metadata) { + const currentTags = new Set(metadata.frontMatter.tags?.filter((tag) => tag?.toLowerCase() != 'zenstack')); + let relatedPosts = allBlogPosts.filter( (post) => - post.metadata.frontMatter.tags - ?.filter((tag) => tag?.toLowerCase() != 'zenstack') - .some((tag) => metadata.frontMatter.tags?.includes(tag)) && post.metadata.title !== metadata.title + post.metadata.frontMatter.tags.some((tag) => currentTags.has(tag)) && post.metadata.title !== metadata.title ); if (relatedPosts.length < MIN_RELATED_POSTS) { remainingCount = MIN_RELATED_POSTS - relatedPosts.length; const remainingPosts = getMultipleRandomElement( - allBlogPosts.filter((post) => !relatedPosts.includes(post)), + allBlogPosts.filter((post) => !relatedPosts.includes(post) && post.metadata.title !== metadata.title), remainingCount ); relatedPosts = relatedPosts.concat(remainingPosts); @@ -37,6 +37,7 @@ function getRelatedPosts(allBlogPosts, metadata) { authors: post.metadata.authors, readingTime: post.metadata.readingTime, date: post.metadata.date, + relatedWeight: post.metadata.frontMatter.tags.filter((tag) => currentTags.has(tag)).length * 3 + 1, }; }); @@ -53,11 +54,11 @@ async function blogPluginExtended(...pluginArgs) { await blogPluginInstance.contentLoaded(data); const { content: blogContents, actions } = data; const { blogPosts: allBlogPosts } = blogContents; - const { addRoute, createData } = actions; + const { createData } = actions; // Create routes for blog entries. await Promise.all( allBlogPosts.map(async (blogPost) => { - const { id, metadata } = blogPost; + const { metadata } = blogPost; const relatedPosts = getRelatedPosts(allBlogPosts, metadata); await createData( // Note that this created data path must be in sync with diff --git a/src/theme/BlogPostPage/index.js b/src/theme/BlogPostPage/index.js index 8ef412d3..c4da0bb2 100644 --- a/src/theme/BlogPostPage/index.js +++ b/src/theme/BlogPostPage/index.js @@ -11,10 +11,38 @@ import Unlisted from '@theme/Unlisted'; import GiscusComponent from '@site/src/components/GiscusComponent'; import { PostPaginator } from '@site/src/components/blog/post-paginator'; -function getMultipleRandomElement(arr, num) { - const shuffled = [...arr].sort(() => 0.5 - Math.random()); +function getMultipleRandomPosts(relatedPosts, number) { + // Create a copy of the original array to avoid modifying it + const weightedItems = [...relatedPosts]; + const result = []; - return shuffled.slice(0, num); + // Calculate the total weight + let totalWeight = weightedItems.reduce((sum, item) => sum + item.relatedWeight, 0); + + while (weightedItems.length > 0) { + // Generate a random value between 0 and the total weight + const randomValue = Math.random() * totalWeight; + let weightSum = 0; + let selectedIndex = -1; + + // Find the item that corresponds to the random value + for (let i = 0; i < weightedItems.length; i++) { + weightSum += weightedItems[i].relatedWeight; + if (randomValue <= weightSum) { + selectedIndex = i; + break; + } + } + + // If an item was selected, add it to the result and remove it from the original array + if (selectedIndex !== -1) { + const [selectedItem] = weightedItems.splice(selectedIndex, 1); + result.push(selectedItem); + totalWeight -= selectedItem.relatedWeight; + } + } + + return result.slice(0, number); } function BlogPostPageContent({ children }) { @@ -28,7 +56,11 @@ function BlogPostPageContent({ children }) { toc_max_heading_level: tocMaxHeadingLevel, } = frontMatter; - const randomThreeRelatedPosts = getMultipleRandomElement(relatedPosts, 3); + const randomThreeRelatedPosts = getMultipleRandomPosts(relatedPosts, 3); + + console.log('relatedPosts', relatedPosts); + + //const url = '/blog/supabase-alternative/cover.png'; return ( {unlisted && } + {children} From cb5724d4c9a1a1e174ec44f68e6482c421d9fd4a Mon Sep 17 00:00:00 2001 From: JG Date: Tue, 3 Sep 2024 13:12:50 +0800 Subject: [PATCH 7/7] display max 3 lines for description --- src/components/blog/post-paginator.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/components/blog/post-paginator.tsx b/src/components/blog/post-paginator.tsx index 0468a7fb..e9ba69ec 100644 --- a/src/components/blog/post-paginator.tsx +++ b/src/components/blog/post-paginator.tsx @@ -49,7 +49,18 @@ export const PostPaginator = ({ posts, title }) => { >
{post.title}
-

{post.description}

+

+ {post.description} +

))}