diff --git a/apps/web/app/(marketing)/blog/page.tsx b/apps/web/app/(marketing)/blog/page.tsx index 0b1becd36..89be0c7c1 100644 --- a/apps/web/app/(marketing)/blog/page.tsx +++ b/apps/web/app/(marketing)/blog/page.tsx @@ -23,7 +23,7 @@ async function BlogPage({ searchParams }: { searchParams: { page: string } }) { const limit = 10; const offset = page * limit; - const posts = await cms.getContentItems({ + const { items: posts, total } = await cms.getContentItems({ collection: 'posts', limit, offset, diff --git a/apps/web/app/(marketing)/docs/layout.tsx b/apps/web/app/(marketing)/docs/layout.tsx index fdc54c06e..0b6c81515 100644 --- a/apps/web/app/(marketing)/docs/layout.tsx +++ b/apps/web/app/(marketing)/docs/layout.tsx @@ -5,12 +5,10 @@ import { DocsNavigation } from '~/(marketing)/docs/_components/docs-navigation'; async function DocsLayout({ children }: React.PropsWithChildren) { const cms = await createCmsClient(); - const pages = await cms.getContentItems({ + const { items: pages } = await cms.getContentItems({ collection: 'documentation', }); - console.log(pages); - return (
diff --git a/apps/web/app/(marketing)/docs/page.tsx b/apps/web/app/(marketing)/docs/page.tsx index 68c2e5aee..f4d02cbbf 100644 --- a/apps/web/app/(marketing)/docs/page.tsx +++ b/apps/web/app/(marketing)/docs/page.tsx @@ -18,12 +18,12 @@ async function DocsPage() { const client = await createCmsClient(); const { t } = await createI18nServerInstance(); - const docs = await client.getContentItems({ + const { items } = await client.getContentItems({ collection: 'documentation', }); // Filter out any docs that have a parentId, as these are children of other docs - const cards = docs.filter((item) => !item.parentId); + const cards = items.filter((item) => !item.parentId); return ( @@ -33,7 +33,7 @@ async function DocsPage() { subtitle={t('marketing:documentationSubtitle')} /> -
+
diff --git a/apps/web/app/server-sitemap.xml/route.ts b/apps/web/app/server-sitemap.xml/route.ts index bce59c57e..ed6756b41 100644 --- a/apps/web/app/server-sitemap.xml/route.ts +++ b/apps/web/app/server-sitemap.xml/route.ts @@ -9,14 +9,14 @@ invariant(appConfig.url, 'No NEXT_PUBLIC_SITE_URL environment variable found'); export async function GET() { const urls = getSiteUrls(); - const client = await createCmsClient(); - const contentItems = await client.getContentItems(); + + const items = await getAllItems(); return getServerSideSitemap([ ...urls, - ...contentItems.map((item) => { + ...items.map((path) => { return { - loc: new URL(item.url, appConfig.url).href, + loc: new URL(path, appConfig.url).href, lastmod: new Date().toISOString(), }; }), @@ -33,3 +33,23 @@ function getSiteUrls() { }; }); } + +async function getAllItems() { + const client = await createCmsClient(); + + const posts = client + .getContentItems({ + collection: 'posts', + }) + .then((response) => response.items) + .then((posts) => posts.map((post) => `/blog/${post.slug}`)); + + const docs = client + .getContentItems({ + collection: 'documentation', + }) + .then((response) => response.items) + .then((docs) => docs.map((doc) => `/docs/${doc.slug}`)); + + return Promise.all([posts, docs]).then((items) => items.flat()); +} diff --git a/packages/cms/core/src/cms-client.ts b/packages/cms/core/src/cms-client.ts index 691a92058..0fd56bcea 100644 --- a/packages/cms/core/src/cms-client.ts +++ b/packages/cms/core/src/cms-client.ts @@ -59,9 +59,10 @@ export abstract class CmsClient { * @param options - Options for filtering and pagination. * @returns A promise that resolves to an array of content items. */ - abstract getContentItems( - options?: Cms.GetContentItemsOptions, - ): Promise; + abstract getContentItems(options?: Cms.GetContentItemsOptions): Promise<{ + total: number; + items: Cms.ContentItem[]; + }>; /** * Retrieves a content item by its ID and type. diff --git a/packages/cms/keystatic/src/client.ts b/packages/cms/keystatic/src/client.ts index d5125ea3f..f134e9718 100644 --- a/packages/cms/keystatic/src/client.ts +++ b/packages/cms/keystatic/src/client.ts @@ -22,38 +22,40 @@ export class KeystaticClient implements CmsClient { const startOffset = options?.offset ?? 0; const endOffset = startOffset + (options?.limit ?? 10); - return Promise.all( - docs - .filter((item) => { - const categoryMatch = options?.categories - ? options.categories.find((category) => - item.entry.categories.includes(category), - ) - : true; + const filtered = docs.filter((item) => { + const categoryMatch = options?.categories + ? options.categories.find((category) => + item.entry.categories.includes(category), + ) + : true; - if (!categoryMatch) { - return false; - } + if (!categoryMatch) { + return false; + } - const tagMatch = options?.tags - ? options.tags.find((tag) => item.entry.tags.includes(tag)) - : true; + const tagMatch = options?.tags + ? options.tags.find((tag) => item.entry.tags.includes(tag)) + : true; - if (!tagMatch) { - return false; - } + if (!tagMatch) { + return false; + } - return true; - }) - .slice(startOffset, endOffset) - .map(async (item) => { - const children = docs.filter( - (item) => item.entry.parent === item.slug, - ); + return true; + }); - return this.mapPost(item, children); - }), + const items = await Promise.all( + filtered.slice(startOffset, endOffset).map(async (item) => { + const children = docs.filter((item) => item.entry.parent === item.slug); + + return this.mapPost(item, children); + }), ); + + return { + total: filtered.length, + items, + }; } async getContentItemBySlug(params: { slug: string; collection: string }) { diff --git a/packages/cms/wordpress/src/wp-client.ts b/packages/cms/wordpress/src/wp-client.ts index f5a41de52..1428ef965 100644 --- a/packages/cms/wordpress/src/wp-client.ts +++ b/packages/cms/wordpress/src/wp-client.ts @@ -79,12 +79,14 @@ export class WordpressClient implements CmsClient { const url = `${this.apiUrl}${endpoint}`; - const posts = await fetch(url).then( - (value) => value.json() as Promise, - ); + const response = await fetch(url); + const totalHeader = response.headers.get('X-WP-Total'); - return Promise.all( - posts.map(async (item: WP_REST_API_Post) => { + const total = totalHeader ? Number(totalHeader) : 0; + const results = (await response.json()) as WP_REST_API_Post[]; + + const posts = await Promise.all( + results.map(async (item: WP_REST_API_Post) => { let parentId: string | undefined; if (!item) { @@ -117,6 +119,11 @@ export class WordpressClient implements CmsClient { }; }), ); + + return { + total, + items: posts, + }; } async getContentItemBySlug({ diff --git a/supabase/migrations/20221215192558_schema.sql b/supabase/migrations/20221215192558_schema.sql index 344e99857..bcb273a97 100644 --- a/supabase/migrations/20221215192558_schema.sql +++ b/supabase/migrations/20221215192558_schema.sql @@ -173,6 +173,7 @@ create policy "public config can be read by authenticated users" on for select to authenticated using (true); +-- Function to get the config settings create or replace function public.get_config() returns json as $$ @@ -215,8 +216,7 @@ end $$ language plpgsql; --- Automatically set user tracking on tables when a row is inserted or --- updated +-- Automatically set user tracking on tables when a row is inserted or updated create or replace function public.trigger_set_user_tracking() returns trigger as $$ @@ -240,6 +240,7 @@ language plpgsql; grant execute on function public.get_config() to authenticated, service_role; +-- check if a field is set in the config create or replace function public.is_set(field_name text) returns boolean as $$ @@ -278,6 +279,7 @@ create table if not exists public.accounts( created_by uuid references auth.users, updated_by uuid references auth.users, picture_url varchar(1000), + public_data jsonb default '{}'::jsonb not null, primary key (id) ); @@ -1733,6 +1735,7 @@ public.is_set( 'enable_team_accounts') and public.accounts.is_personal_account = false); +-- Function: create an invitation to an account create or replace function public.create_invitation(account_id uuid, email text, role varchar(50)) returns public.invitations @@ -1765,30 +1768,10 @@ end; $$ language plpgsql; -create or replace function public.get_user_accounts() - returns setof public.accounts - as $$ -begin - select - id, - name, - picture_url - from - public.accounts - join public.accounts_memberships on accounts.id = - accounts_memberships.account_id - where - accounts_memberships.user_id = auth.uid(); - -end; - -$$ -language plpgsql; - +-- +-- VIEW "user_account_workspace": -- we create a view to load the general app data for the authenticated --- user --- which includes the user's accounts, memberships, and roles, and --- relative subscription status +-- user which includes the user's accounts, memberships, and roles, and relative subscription status create or replace view public.user_account_workspace as select accounts.id as id, @@ -1804,6 +1787,10 @@ where grant select on public.user_account_workspace to authenticated, service_role; +-- +-- VIEW "user_accounts": +-- we create a view to load the user's accounts and memberships +-- useful to display the user's accounts in the app create or replace view public.user_accounts as select accounts.id as id, @@ -1821,6 +1808,9 @@ where grant select on public.user_accounts to authenticated, service_role; +-- +-- Function: get the account workspace for an organization account +-- to load all the required data for the authenticated user within the account scope create or replace function public.organization_account_workspace(account_slug text) returns table( @@ -1872,6 +1862,7 @@ language plpgsql; grant execute on function public.organization_account_workspace(text) to authenticated, service_role; +-- Functions: get account members create or replace function public.get_account_members(account_slug text) returns table( id uuid, @@ -1961,7 +1952,6 @@ create type kit.invitation as ( email text, role varchar( 50)); --- Then, modify your function to use this type create or replace function public.add_invitations_to_account(account_slug text, invitations kit.invitation[])