Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/workflows/build-artifacts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,21 @@ jobs:
cp content-temp/scripts/tina-migration/redirects.json ./
mv content-temp ./content

- name: Setup Node.js (author index)
uses: actions/setup-node@v4
with:
node-version: "20"

- name: Setup pnpm (author index)
uses: pnpm/action-setup@v4
with:
version: 10.10.0

- name: Generate author rules index
env:
LOCAL_CONTENT_RELATIVE_PATH: ../content
run: pnpm run generate:author-rules

# ────────────────────────────── AZURE LOGIN ───────────────────────────
- name: Azure CLI – Login
uses: azure/login@v2
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,4 @@ rule-to-categories.json
orphaned_rules.json
archived-rules.json
redirects.json
public/author-title-to-rules-map.json
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,14 +99,15 @@ To deploy a PR preview, add a comment to the PR: **`/deploy`**. This creates/upd
#### In this Repository (Website)

- **`build-rule-category-map.py`**
Generates two JSON files:
Generates JSON files used by the website:
- `rule-to-categories.json` (maps rules to categories)
- `category-uri-title-map.json` (maps category URIs to titles)
- `orphaned_rules.json` (maps category URIs to titles)
- `orphaned_rules.json` (orphaned rules report)
- `redirects.json` (old -> new URI redirects)
Reads rule data from the `SSW.Rules.Content` repo and runs during the build process (via GitHub Actions) or manually from `scripts/tina-migration`.

- **`prepare-content.js`**
A Node.js script that runs `build-rule-category-map.py` and moves the JSON files to the correct location for use by the website.
A Node.js script that runs the content repo Python scripts, moves the generated JSON files into this repo, and runs `pnpm run generate:author-rules` to create `public/author-title-to-rules-map.json` for `/api/tina/rules-by-author`.
Uses the `LOCAL_CONTENT_RELATIVE_PATH` environment variable to locate the content repo.

#### In the Content Repository
Expand Down
111 changes: 103 additions & 8 deletions app/api/tina/rules-by-author/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,42 @@
import fs from "fs";
import { join } from "path";
import { NextRequest, NextResponse } from "next/server";
import client from "@/tina/__generated__/client";

export const runtime = "nodejs";

const AUTHOR_INDEX_FILENAME = "author-title-to-rules-map.json";

type AuthorIndex = Record<string, string[]>;

let cachedIndex: AuthorIndex | null | undefined;

const getAuthorIndex = (): AuthorIndex | null => {
if (cachedIndex !== undefined) return cachedIndex;

const indexPath = join(process.cwd(), "public", AUTHOR_INDEX_FILENAME);
try {
const raw = fs.readFileSync(indexPath, "utf8");
cachedIndex = JSON.parse(raw) as AuthorIndex;
} catch {
cachedIndex = null;
}

return cachedIndex;
};

const encodeCursor = (index: number) => Buffer.from(String(index), "utf8").toString("base64");

const decodeCursor = (cursor: string) => {
try {
const decoded = Buffer.from(cursor, "base64").toString("utf8");
const n = Number(decoded);
return Number.isFinite(n) ? n : -1;
} catch {
return -1;
}
};

export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
Expand All @@ -12,17 +48,76 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: "authorTitle is required" }, { status: 400 });
}

const first = firstStr ? Number(firstStr) : undefined;
const first = firstStr ? Number(firstStr) : 50;

const index = getAuthorIndex();
console.log("??", index)
if (!index) {
// Backward-compatible fallback (slow path) if index isn't present in the build.
const result = await client.queries.rulesByAuthor({ authorTitle, first, after });
return NextResponse.json(result, { status: 200 });
}

const authorFiles = index[authorTitle] ?? [];

const afterIndex = after ? decodeCursor(after) : -1;
if (after && afterIndex === -1) {
// Cursor from the old Tina-backed implementation.
const result = await client.queries.rulesByAuthor({ authorTitle, first, after });
return NextResponse.json(result, { status: 200 });
}

const start = Math.max(0, afterIndex + 1);
const size = Number.isFinite(first) && first > 0 ? first : 50;

const endExclusive = Math.min(start + size, authorFiles.length);
const pageFiles = authorFiles.slice(start, endExclusive);

const nodes = await Promise.all(
pageFiles.map(async (relativePath) => {
try {
const res = await client.queries.ruleDataBasic({ relativePath });
const rule: any = res?.data?.rule;
if (!rule || typeof rule.guid !== "string") return null;

return {
guid: rule.guid,
title: rule.title,
uri: rule.uri,
body: rule.body,
authors: (rule.authors ?? [])
.map((a: any) => (a && a.title ? { title: a.title } : null))
.filter((a: any) => a !== null),
lastUpdated: rule.lastUpdated,
lastUpdatedBy: rule.lastUpdatedBy,
};
} catch {
return null;
}
})
);

const edges = nodes.filter(Boolean).map((node) => ({ node }));

const result = await client.queries.rulesByAuthor({
authorTitle,
first,
after,
});
const hasNextPage = endExclusive < authorFiles.length;
const endCursor = edges.length > 0 ? encodeCursor(endExclusive - 1) : null;

return NextResponse.json(result, { status: 200 });
return NextResponse.json(
{
data: {
ruleConnection: {
pageInfo: {
hasNextPage,
endCursor,
},
edges,
},
},
},
{ status: 200 }
);
} catch (error) {
console.error("Error fetching rules by author from Tina:", error);
console.error("Error fetching rules by author:", error);
return NextResponse.json({ error: "Failed to fetch rules", details: error instanceof Error ? error.message : String(error) }, { status: 500 });
}
}
13 changes: 5 additions & 8 deletions app/user/client-page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
"use client";

import { useEffect, useState, useMemo } from "react";
import { useSearchParams } from "next/navigation";
import Breadcrumbs from "@/components/Breadcrumbs";
import Spinner from "@/components/Spinner";
import { useEffect, useMemo, useState } from "react";
import { FaUserCircle } from "react-icons/fa";
import Breadcrumbs from "@/components/Breadcrumbs";
import RuleList from "@/components/rule-list";
import Spinner from "@/components/Spinner";
import Pagination from "@/components/ui/pagination";

const Tabs = {
Expand All @@ -15,7 +15,7 @@ const Tabs = {

type TabKey = (typeof Tabs)[keyof typeof Tabs];

export default function UserRulesClientPage({ ruleCount }) {
export default function UserRulesClientPage() {
const searchParams = useSearchParams();
const [activeTab, setActiveTab] = useState<TabKey>(Tabs.AUTHORED);
const queryStringRulesAuthor = searchParams.get("author") || "";
Expand All @@ -30,13 +30,10 @@ export default function UserRulesClientPage({ ruleCount }) {
const [authoredRules, setAuthoredRules] = useState<any[]>([]);
const [author, setAuthor] = useState<{ fullName?: string; slug?: string; gitHubUrl?: string }>({});
const [loadingAuthored, setLoadingAuthored] = useState(false);
const [authoredNextCursor, setAuthoredNextCursor] = useState<string | null>(null);
const [authoredHasNext, setAuthoredHasNext] = useState(false);
const [loadingMoreAuthored, setLoadingMoreAuthored] = useState(false);
const [githubError, setGithubError] = useState<string | null>(null);
const [currentPageAuthored, setCurrentPageAuthored] = useState(1);
const [itemsPerPageAuthored, setItemsPerPageAuthored] = useState(20);
const FETCH_PAGE_SIZE = 10;
const FETCH_PAGE_SIZE = 100;

const resolveAuthor = async (): Promise<string> => {
const res = await fetch(`./api/crm/employees?query=${encodeURIComponent(queryStringRulesAuthor)}`);
Expand Down
21 changes: 8 additions & 13 deletions app/user/page.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,17 @@
import Layout from '@/components/layout/layout';
import { Section } from '@/components/layout/section';
import UserRulesClientPage from './client-page';
import { Suspense } from "react";
import { fetchRuleCount } from '@/lib/services/rules';
import { siteUrl } from '@/site-config';
import { Section } from "@/components/layout/section";
import { siteUrl } from "@/site-config";
import UserRulesClientPage from "./client-page";

export const revalidate = 300;

export default async function UserRulesPage() {

const ruleCount = await fetchRuleCount()

return (
<Section>
<Suspense fallback={null}>
<UserRulesClientPage ruleCount={ruleCount} />
</Suspense>
</Section>
<Section>
<Suspense fallback={null}>
<UserRulesClientPage />
</Suspense>
</Section>
);
}

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"lint": "biome lint",
"dev:build": "next build",
"prepare:content": "cross-env node ./scripts/prepare-content.js",
"generate:author-rules": "node ./scripts/generate-author-rules-map.js",
"crawl-sitemap": "node ./scripts/crawl-sitemap.js"
},
"devDependencies": {
Expand Down
Loading