@openinc/parse-server-opendash 3.21.0 → 3.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,5 +17,5 @@ export declare class DocumentationImporter {
17
17
  /**
18
18
  * Get file content from GitHub
19
19
  */
20
- getFileContent(owner: string, repo: string, path: string, ref?: string): Promise<any>;
20
+ getFileContent(path: string): Promise<any>;
21
21
  }
@@ -19,13 +19,13 @@ class DocumentationImporter {
19
19
  // Validate token
20
20
  const gitUser = await this.githubClient.validateToken();
21
21
  // Get repository info
22
- const repo = await this.githubClient.getRepository(options.organization, options.repository);
22
+ const repo = await this.githubClient.getRepository();
23
23
  // Determine branch to use
24
24
  const branch = options.branch || repo.default_branch;
25
25
  // Validate branch exists
26
26
  await this.validateBranch(options.organization, options.repository, branch);
27
27
  // Get branch info (commit SHA)
28
- const branchInfo = await this.githubClient.getBranch(options.organization, options.repository, branch);
28
+ const branchInfo = await this.githubClient.getBranch();
29
29
  const latestCommitSha = branchInfo.commit.sha;
30
30
  // Attempt to load previously imported commit SHA from Parse
31
31
  let previousCommitSha;
@@ -59,7 +59,7 @@ class DocumentationImporter {
59
59
  },
60
60
  };
61
61
  }
62
- const treeData = await this.githubClient.getTree(options.organization, options.repository, latestCommitSha);
62
+ const treeData = await this.githubClient.getTree(latestCommitSha);
63
63
  // Organize structure with filtering and root path
64
64
  const structure = await Organizer_1.DocumentationOrganizer.organizeRepositoryStructure(treeData, options.fileFilter, options.rootPath, this.githubClient, options.organization, options.repository, branch, options.defaultFolderConfig);
65
65
  // Clean up old documentation that is no longer in the repository
@@ -113,7 +113,7 @@ class DocumentationImporter {
113
113
  */
114
114
  async validateBranch(owner, repo, branch) {
115
115
  try {
116
- const branches = await this.githubClient.getBranches(owner, repo);
116
+ const branches = await this.githubClient.getBranches();
117
117
  const branchNames = branches.map((b) => b.name);
118
118
  if (!branchNames.includes(branch)) {
119
119
  throw new Error(`Branch '${branch}' not found. Available branches: ${branchNames.join(", ")}`);
@@ -126,8 +126,8 @@ class DocumentationImporter {
126
126
  /**
127
127
  * Get file content from GitHub
128
128
  */
129
- async getFileContent(owner, repo, path, ref) {
130
- return this.githubClient.getFileContent(owner, repo, path, ref);
129
+ async getFileContent(path) {
130
+ return this.githubClient.getFileContent(path);
131
131
  }
132
132
  }
133
133
  exports.DocumentationImporter = DocumentationImporter;
@@ -20,7 +20,7 @@ class DocumentationOrganizer {
20
20
  // 2. Build folder structure & identify config.json files
21
21
  const { root: builtRoot, configFiles } = StructureBuilder_1.StructureBuilder.build(normalizedFiles, rootPath, defaultFolderConfig);
22
22
  // 3. Apply configs
23
- await ConfigApplier_1.ConfigApplier.apply(builtRoot, configFiles, githubClient, owner, repo, branch, rootPath);
23
+ await ConfigApplier_1.ConfigApplier.apply(builtRoot, configFiles, githubClient, branch, rootPath);
24
24
  // 4. Apply feature filtering
25
25
  console.log(`[DocumentationOrganizer] Applying feature-based filtering...`);
26
26
  const preFilterFileCount = normalizedFiles.length;
@@ -35,9 +35,9 @@ class DocumentationOrganizer {
35
35
  }
36
36
  // 5. Enrich commit metadata
37
37
  if (githubClient && owner && repo) {
38
- await MetadataEnricher_1.MetadataEnricher.enrichCommits(filteredFiles, githubClient, owner, repo, branch, rootPath);
38
+ await MetadataEnricher_1.MetadataEnricher.enrichCommits(filteredFiles, githubClient, rootPath);
39
39
  // 6. Load content
40
- await ContentLoader_1.ContentLoader.populate(filteredFiles, githubClient, owner, repo, branch, rootPath);
40
+ await ContentLoader_1.ContentLoader.populate(filteredFiles, githubClient, branch, rootPath);
41
41
  }
42
42
  // Build extension map
43
43
  const filesByExtension = new Map();
@@ -6,6 +6,7 @@ const core_1 = require("../core");
6
6
  const config_1 = require("../config");
7
7
  const Converter_1 = require("../core/Converter");
8
8
  const types_1 = require("../../../types");
9
+ const LinkResolver_1 = require("../services/LinkResolver");
9
10
  const GITHUB_TOKEN = process.env.OI_DOCUMENTATION_GITHUB_ACCESS_TOKEN;
10
11
  async function importDocs() {
11
12
  try {
@@ -23,7 +24,9 @@ async function importDocs() {
23
24
  .equalTo("objectId", userId)
24
25
  .include("tenant")
25
26
  .first({ useMasterKey: true })
26
- : undefined;
27
+ : await new Parse.Query(Parse.User)
28
+ .ascending("createdAt")
29
+ .first({ useMasterKey: true });
27
30
  const tenantId = process.env.OI_DOCUMENTATION_TENANT_ID;
28
31
  const tenant = tenantId
29
32
  ? new types_1.Tenant({ objectId: tenantId })
@@ -31,6 +34,9 @@ async function importDocs() {
31
34
  const result = await importer.importFromRepository(config, user, tenant);
32
35
  if (result.metadata.skipped)
33
36
  return;
37
+ // resolve links in all files and fetch their content into
38
+ const linkResolver = new LinkResolver_1.LinkResolver(GITHUB_TOKEN, result.structure?.allFiles);
39
+ await linkResolver.resolveLinks(user, tenant);
34
40
  if (result.structure)
35
41
  await new Converter_1.DocumentationConverter(result).convert(user, tenant);
36
42
  }
@@ -1,7 +1,7 @@
1
1
  import { DocumentationFolder, DocumentationFile } from "../types";
2
2
  import { GitHubClient } from "./GitHubClient";
3
3
  export declare class ConfigApplier {
4
- static apply(root: DocumentationFolder, configFiles: DocumentationFile[], githubClient?: GitHubClient, owner?: string, repo?: string, branch?: string, rootPath?: string): Promise<void>;
4
+ static apply(root: DocumentationFolder, configFiles: DocumentationFile[], githubClient?: GitHubClient, branch?: string, rootPath?: string): Promise<void>;
5
5
  /** Compare raw vs sanitized folder config and log warnings for dropped / invalid entries */
6
6
  private static diffAndWarnFolderConfig;
7
7
  private static diffAndWarnFileConfig;
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ConfigApplier = void 0;
4
4
  const config_1 = require("../config");
5
5
  class ConfigApplier {
6
- static async apply(root, configFiles, githubClient, owner, repo, branch, rootPath) {
6
+ static async apply(root, configFiles, githubClient, branch, rootPath) {
7
7
  if (!configFiles.length)
8
8
  return;
9
9
  for (const cfgFile of configFiles) {
@@ -16,8 +16,8 @@ class ConfigApplier {
16
16
  if (!folder)
17
17
  continue;
18
18
  try {
19
- if (githubClient && owner && repo) {
20
- const customRaw = await this.fetchConfigFromGitHub(cfgFile, githubClient, owner, repo, branch, rootPath);
19
+ if (githubClient) {
20
+ const customRaw = await this.fetchConfigFromGitHub(cfgFile, githubClient, branch, rootPath);
21
21
  const custom = (0, config_1.sanitizeFolderConfig)(customRaw);
22
22
  this.diffAndWarnFolderConfig(customRaw, custom, cfgFile.path);
23
23
  folder.config = { ...folder.config, ...custom };
@@ -74,12 +74,12 @@ class ConfigApplier {
74
74
  console.warn(`[ConfigApplier] Invalid type for 'locations' (expected string[]) in file '${fileBase}' (${sourcePath})`);
75
75
  }
76
76
  }
77
- static async fetchConfigFromGitHub(cfgFile, githubClient, owner, repo, branch, rootPath) {
77
+ static async fetchConfigFromGitHub(cfgFile, githubClient, branch, rootPath) {
78
78
  const normalizedRoot = rootPath?.replace(/^\/+/g, "").replace(/\/+$/g, "");
79
79
  const fullPath = normalizedRoot
80
80
  ? `${normalizedRoot}/${cfgFile.path}`
81
81
  : cfgFile.path;
82
- const fileContent = await githubClient.getFileContent(owner, repo, fullPath, branch);
82
+ const fileContent = await githubClient.getFileContent(fullPath);
83
83
  const json = Buffer.from(fileContent.content, "base64").toString("utf-8");
84
84
  return JSON.parse(json);
85
85
  }
@@ -2,5 +2,5 @@ import { DocumentationFile } from "../types";
2
2
  import { GitHubClient } from "./GitHubClient";
3
3
  export declare class ContentLoader {
4
4
  private static readonly TEXT_EXTENSIONS;
5
- static populate(files: DocumentationFile[], githubClient: GitHubClient, owner: string, repo: string, branch?: string, rootPath?: string): Promise<void>;
5
+ static populate(files: DocumentationFile[], githubClient: GitHubClient, branch?: string, rootPath?: string): Promise<void>;
6
6
  }
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ContentLoader = void 0;
4
4
  class ContentLoader {
5
- static async populate(files, githubClient, owner, repo, branch, rootPath) {
5
+ static async populate(files, githubClient, branch, rootPath) {
6
6
  const candidates = files.filter((f) => this.TEXT_EXTENSIONS.has(f.extension));
7
7
  if (!candidates.length)
8
8
  return;
@@ -13,7 +13,7 @@ class ContentLoader {
13
13
  const fullPath = normalizedRoot
14
14
  ? `${normalizedRoot}/${file.path}`
15
15
  : file.path;
16
- const ghContent = await githubClient.getFileContent(owner, repo, fullPath, branch);
16
+ const ghContent = await githubClient.getFileContent(fullPath);
17
17
  if (ghContent?.content) {
18
18
  file.content = Buffer.from(ghContent.content, "base64").toString("utf-8");
19
19
  fetched++;
@@ -1,11 +1,16 @@
1
- import { GitHubTreeResponse, GitHubBranch, GitHubRepository } from "../types";
1
+ import { GitHubTreeResponse, GitHubBranch, GitHubRepository, ImportOptions } from "../types";
2
2
  /**
3
3
  * GitHub API Client for fetching repository data
4
4
  */
5
5
  export declare class GitHubClient {
6
6
  private readonly baseUrl;
7
7
  private readonly headers;
8
- constructor(token: string);
8
+ private readonly importConfig;
9
+ private branch;
10
+ private repository;
11
+ private tree;
12
+ private sha;
13
+ constructor(token: string, importConfig?: Partial<ImportOptions>);
9
14
  /**
10
15
  * Validate the GitHub token
11
16
  */
@@ -15,29 +20,36 @@ export declare class GitHubClient {
15
20
  /**
16
21
  * Get repository information
17
22
  */
18
- getRepository(owner: string, repo: string): Promise<GitHubRepository>;
23
+ getRepository(): Promise<GitHubRepository>;
19
24
  /**
20
25
  * Get all branches for a repository
21
26
  */
22
- getBranches(owner: string, repo: string): Promise<GitHubBranch[]>;
27
+ getBranches(): Promise<GitHubBranch[]>;
23
28
  /**
24
29
  * Get a specific branch
25
30
  */
26
- getBranch(owner: string, repo: string, branch: string): Promise<GitHubBranch>;
31
+ getBranch(): Promise<GitHubBranch>;
27
32
  /**
28
33
  * Get repository tree (file structure)
29
34
  */
30
- getTree(owner: string, repo: string, sha: string, recursive?: boolean): Promise<GitHubTreeResponse>;
35
+ getTree(sha: string, recursive?: boolean): Promise<GitHubTreeResponse>;
31
36
  /**
32
37
  * Get file content
33
38
  */
34
- getFileContent(owner: string, repo: string, path: string, ref?: string): Promise<any>;
39
+ getFileContent(path: string): Promise<any>;
35
40
  /**
36
41
  * Get the last commit information for a specific file
37
42
  */
38
- getFileLastCommit(owner: string, repo: string, path: string, ref?: string): Promise<any>;
43
+ getFileLastCommit(path: string, ref?: string): Promise<any>;
39
44
  /**
40
45
  * Get commit information for multiple files in batch
41
46
  */
42
- getMultipleFileCommits(owner: string, repo: string, paths: string[], ref?: string): Promise<Map<string, any>>;
47
+ getMultipleFileCommits(paths: string[]): Promise<Map<string, any>>;
48
+ /**
49
+ * Fetches the content of each linked file from GitHub.
50
+ * @param ref Branch or commit SHA
51
+ * @param paths Array of relative file paths (from repo root)
52
+ * @returns Map of path to file content (decoded as string if possible)
53
+ */
54
+ private fetchLinkedFiles;
43
55
  }
@@ -1,17 +1,23 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.GitHubClient = void 0;
4
+ const config_1 = require("../config");
4
5
  /**
5
6
  * GitHub API Client for fetching repository data
6
7
  */
7
8
  class GitHubClient {
8
- constructor(token) {
9
+ constructor(token, importConfig = config_1.DEFAULT_IMPORT_CONFIG) {
9
10
  this.baseUrl = "https://api.github.com";
11
+ this.branch = null;
12
+ this.repository = null;
13
+ this.tree = null;
14
+ this.sha = null;
10
15
  this.headers = {
11
16
  Authorization: `Bearer ${token}`,
12
17
  Accept: "application/vnd.github.v3+json",
13
18
  "User-Agent": "OpenInc-Documentation-Importer",
14
19
  };
20
+ this.importConfig = importConfig;
15
21
  }
16
22
  /**
17
23
  * Validate the GitHub token
@@ -29,8 +35,9 @@ class GitHubClient {
29
35
  /**
30
36
  * Get repository information
31
37
  */
32
- async getRepository(owner, repo) {
33
- const response = await fetch(`${this.baseUrl}/repos/${owner}/${repo}`, {
38
+ async getRepository() {
39
+ const { organization, repository } = this.importConfig;
40
+ const response = await fetch(`${this.baseUrl}/repos/${organization}/${repository}`, {
34
41
  headers: this.headers,
35
42
  });
36
43
  if (!response.ok) {
@@ -42,8 +49,9 @@ class GitHubClient {
42
49
  /**
43
50
  * Get all branches for a repository
44
51
  */
45
- async getBranches(owner, repo) {
46
- const response = await fetch(`${this.baseUrl}/repos/${owner}/${repo}/branches`, {
52
+ async getBranches() {
53
+ const { organization, repository } = this.importConfig;
54
+ const response = await fetch(`${this.baseUrl}/repos/${organization}/${repository}/branches`, {
47
55
  headers: this.headers,
48
56
  });
49
57
  if (!response.ok) {
@@ -54,8 +62,9 @@ class GitHubClient {
54
62
  /**
55
63
  * Get a specific branch
56
64
  */
57
- async getBranch(owner, repo, branch) {
58
- const response = await fetch(`${this.baseUrl}/repos/${owner}/${repo}/branches/${branch}`, {
65
+ async getBranch() {
66
+ const { organization, repository, branch } = this.importConfig;
67
+ const response = await fetch(`${this.baseUrl}/repos/${organization}/${repository}/branches/${branch}`, {
59
68
  headers: this.headers,
60
69
  });
61
70
  if (!response.ok) {
@@ -67,8 +76,9 @@ class GitHubClient {
67
76
  /**
68
77
  * Get repository tree (file structure)
69
78
  */
70
- async getTree(owner, repo, sha, recursive = true) {
71
- const url = `${this.baseUrl}/repos/${owner}/${repo}/git/trees/${sha}${recursive ? "?recursive=1" : ""}`;
79
+ async getTree(sha, recursive = true) {
80
+ const { organization, repository } = this.importConfig;
81
+ const url = `${this.baseUrl}/repos/${organization}/${repository}/git/trees/${sha}${recursive ? "?recursive=1" : ""}`;
72
82
  const response = await fetch(url, {
73
83
  headers: this.headers,
74
84
  });
@@ -80,8 +90,9 @@ class GitHubClient {
80
90
  /**
81
91
  * Get file content
82
92
  */
83
- async getFileContent(owner, repo, path, ref) {
84
- const url = `${this.baseUrl}/repos/${owner}/${repo}/contents/${path}${ref ? `?ref=${ref}` : ""}`;
93
+ async getFileContent(path) {
94
+ const { organization, repository, branch } = this.importConfig;
95
+ const url = `${this.baseUrl}/repos/${organization}/${repository}/contents/${path}${`?ref=${branch}`}`;
85
96
  const response = await fetch(url, {
86
97
  headers: this.headers,
87
98
  });
@@ -93,8 +104,9 @@ class GitHubClient {
93
104
  /**
94
105
  * Get the last commit information for a specific file
95
106
  */
96
- async getFileLastCommit(owner, repo, path, ref) {
97
- const url = `${this.baseUrl}/repos/${owner}/${repo}/commits?path=${encodeURIComponent(path)}&per_page=1${ref ? `&sha=${ref}` : ""}`;
107
+ async getFileLastCommit(path, ref) {
108
+ const { organization, repository } = this.importConfig;
109
+ const url = `${this.baseUrl}/repos/${organization}/${repository}/commits?path=${encodeURIComponent(path)}&per_page=1${ref ? `&sha=${ref}` : ""}`;
98
110
  const response = await fetch(url, {
99
111
  headers: this.headers,
100
112
  });
@@ -107,7 +119,7 @@ class GitHubClient {
107
119
  /**
108
120
  * Get commit information for multiple files in batch
109
121
  */
110
- async getMultipleFileCommits(owner, repo, paths, ref) {
122
+ async getMultipleFileCommits(paths) {
111
123
  const commitMap = new Map();
112
124
  // Process files in batches to avoid rate limiting
113
125
  const batchSize = 5;
@@ -115,7 +127,7 @@ class GitHubClient {
115
127
  const batch = paths.slice(i, i + batchSize);
116
128
  const promises = batch.map(async (path) => {
117
129
  try {
118
- const commit = await this.getFileLastCommit(owner, repo, path, ref);
130
+ const commit = await this.getFileLastCommit(path);
119
131
  return { path, commit };
120
132
  }
121
133
  catch (error) {
@@ -136,5 +148,41 @@ class GitHubClient {
136
148
  }
137
149
  return commitMap;
138
150
  }
151
+ /**
152
+ * Fetches the content of each linked file from GitHub.
153
+ * @param ref Branch or commit SHA
154
+ * @param paths Array of relative file paths (from repo root)
155
+ * @returns Map of path to file content (decoded as string if possible)
156
+ */
157
+ async fetchLinkedFiles(ref, paths) {
158
+ const result = new Map();
159
+ for (const path of paths) {
160
+ try {
161
+ const file = await this.getFileContent(path);
162
+ // GitHub returns content as base64 for binary/text files
163
+ if (file && file.content && file.encoding === "base64") {
164
+ const buffer = Buffer.from(file.content, "base64");
165
+ // Try to decode as UTF-8 string, fallback to Buffer for binary
166
+ const asString = buffer.toString("utf8");
167
+ // Heuristic: treat as string if no replacement chars
168
+ if (!asString.includes("\uFFFD")) {
169
+ result.set(path, asString);
170
+ }
171
+ else {
172
+ result.set(path, buffer);
173
+ }
174
+ }
175
+ else {
176
+ // For raw text files, just use the content
177
+ result.set(path, file.content ?? "");
178
+ }
179
+ }
180
+ catch (err) {
181
+ console.warn(`⚠️ Could not fetch ${path}:`, err);
182
+ result.set(path, "");
183
+ }
184
+ }
185
+ return result;
186
+ }
139
187
  }
140
188
  exports.GitHubClient = GitHubClient;
@@ -0,0 +1,10 @@
1
+ import { _User, Tenant } from "../../../types";
2
+ import { DocumentationFile } from "../types";
3
+ export declare class LinkResolver {
4
+ private gitHubClient;
5
+ private files;
6
+ constructor(gitHubToken: string, files?: DocumentationFile[]);
7
+ resolveLinks(user?: _User, tenant?: Tenant): Promise<void>;
8
+ private scanLinksWithPositions;
9
+ private replaceLinksInContent;
10
+ }
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.LinkResolver = void 0;
7
+ const types_1 = require("../../../types");
8
+ const GitHubClient_1 = require("./GitHubClient");
9
+ const path_1 = __importDefault(require("path"));
10
+ class LinkResolver {
11
+ constructor(gitHubToken, files = []) {
12
+ this.files = [];
13
+ this.gitHubClient = new GitHubClient_1.GitHubClient(gitHubToken);
14
+ this.files = files;
15
+ }
16
+ // Returns a map of unique local file/image links to their GitHub content
17
+ async resolveLinks(user, tenant) {
18
+ console.log("[LinkResolver] Resolving links in documentation files...");
19
+ const linkMap = this.scanLinksWithPositions();
20
+ // first we build an array with unique absoluteLinks.
21
+ const uniqueAbsoluteLinks = Array.from(new Set(Array.from(linkMap.values()).flatMap((occs) => occs.map((o) => o.absoluteLink))));
22
+ console.log(`[LinkResolver] Found ${uniqueAbsoluteLinks.length} unique local links to resolve.`);
23
+ // we can now fetch all files for absoluteLinks
24
+ const fetchedFiles = new Map();
25
+ await Promise.all(uniqueAbsoluteLinks.map(async (filePath) => {
26
+ try {
27
+ const file = await this.gitHubClient.getFileContent(filePath);
28
+ if (file && file.content && file.encoding === "base64") {
29
+ const buffer = Buffer.from(file.content, "base64");
30
+ // Try to decode as UTF-8 string, fallback to Buffer for binary
31
+ const asString = buffer.toString("utf8");
32
+ if (!asString.includes("\uFFFD")) {
33
+ fetchedFiles.set(filePath, asString);
34
+ }
35
+ else {
36
+ fetchedFiles.set(filePath, buffer);
37
+ }
38
+ }
39
+ else {
40
+ fetchedFiles.set(filePath, file.content ?? "");
41
+ }
42
+ }
43
+ catch (err) {
44
+ console.warn(`[LinkResolver] ⚠️ Could not fetch ${filePath}:`, err);
45
+ fetchedFiles.set(filePath, "");
46
+ }
47
+ }));
48
+ // old absoluteLink => new file link
49
+ const newLinkMap = new Map();
50
+ // now we can build a map of absoluteLink => new Asset URL
51
+ for (const [absoluteLink, content] of fetchedFiles) {
52
+ const fileName = absoluteLink.split("/").pop();
53
+ // map the data to a Parse File and save as Asset
54
+ const data = typeof content === "string"
55
+ ? { base64: Buffer.from(content, "utf8").toString("base64") }
56
+ : { base64: content.toString("base64") };
57
+ const parseFile = await new Parse.File(fileName || "file", data).save({
58
+ useMasterKey: true,
59
+ });
60
+ const asset = new types_1.Assets({
61
+ file: parseFile,
62
+ context: "documentation",
63
+ tenant,
64
+ user,
65
+ description: fileName,
66
+ });
67
+ const savedAsset = await asset.save(null, { useMasterKey: true });
68
+ const newURL = savedAsset.file?.url() || "";
69
+ newLinkMap.set(absoluteLink, newURL);
70
+ }
71
+ console.log(`[LinkResolver] Resolved and uploaded ${newLinkMap.size} linked assets.`);
72
+ // Finally, replace links in each file's content
73
+ for (const file of this.files) {
74
+ const occurrences = linkMap.get(file.path);
75
+ if (occurrences && occurrences.length) {
76
+ file.content = this.replaceLinksInContent(occurrences, file.content ?? "", newLinkMap);
77
+ }
78
+ }
79
+ console.log("[LinkResolver] Link resolution completed.");
80
+ }
81
+ // Scan all files and return a map: filePath -> array of link occurrences
82
+ scanLinksWithPositions() {
83
+ const regex = /(!?\[[^\]]*\]\(([^)]+)\))/g;
84
+ const result = new Map();
85
+ for (const file of this.files) {
86
+ const occurrences = [];
87
+ const content = file.content || "";
88
+ let match;
89
+ while ((match = regex.exec(content)) !== null) {
90
+ const markdown = match[1];
91
+ const link = match[2].trim();
92
+ // Skip URLs
93
+ if (/^https?:\/\//i.test(link) || /^mailto:/i.test(link))
94
+ continue;
95
+ const baseDir = path_1.default.posix.dirname(file.path);
96
+ const absoluteLink = path_1.default.posix.normalize(path_1.default.posix.join("./docs", baseDir, link));
97
+ occurrences.push({
98
+ filePath: file.path,
99
+ link,
100
+ absoluteLink,
101
+ start: match.index,
102
+ end: match.index + markdown.length,
103
+ markdown,
104
+ });
105
+ }
106
+ if (occurrences.length)
107
+ result.set(file.path, occurrences);
108
+ }
109
+ return result;
110
+ }
111
+ // Replace links in content with new links using positions
112
+ replaceLinksInContent(occurrences, content, assetLinkMap) {
113
+ let offset = 0; // track content length changes
114
+ let newContent = content;
115
+ for (const occ of occurrences) {
116
+ const newLink = assetLinkMap.get(occ.absoluteLink);
117
+ if (!newLink)
118
+ continue;
119
+ // Replace only the link part inside markdown
120
+ const before = newContent.slice(0, occ.start + offset);
121
+ const matchText = newContent.slice(occ.start + offset, occ.end + offset);
122
+ const after = newContent.slice(occ.end + offset);
123
+ // Replace link inside [text](link)
124
+ const replaced = matchText.replace(occ.link, newLink);
125
+ newContent = before + replaced + after;
126
+ offset += replaced.length - matchText.length;
127
+ }
128
+ return newContent;
129
+ }
130
+ }
131
+ exports.LinkResolver = LinkResolver;
@@ -1,5 +1,5 @@
1
1
  import { DocumentationFile } from "../types";
2
2
  import { GitHubClient } from "./GitHubClient";
3
3
  export declare class MetadataEnricher {
4
- static enrichCommits(files: DocumentationFile[], githubClient: GitHubClient, owner: string, repo: string, branch?: string, rootPath?: string): Promise<void>;
4
+ static enrichCommits(files: DocumentationFile[], githubClient: GitHubClient, rootPath?: string): Promise<void>;
5
5
  }
@@ -2,11 +2,11 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MetadataEnricher = void 0;
4
4
  class MetadataEnricher {
5
- static async enrichCommits(files, githubClient, owner, repo, branch, rootPath) {
5
+ static async enrichCommits(files, githubClient, rootPath) {
6
6
  const normalizedRoot = rootPath?.replace(/^\/+/g, "").replace(/\/+$/g, "");
7
7
  const fullPaths = files.map((f) => normalizedRoot ? `${normalizedRoot}/${f.path}` : f.path);
8
8
  try {
9
- const commitMap = await githubClient.getMultipleFileCommits(owner, repo, fullPaths, branch);
9
+ const commitMap = await githubClient.getMultipleFileCommits(fullPaths);
10
10
  let enriched = 0;
11
11
  files.forEach((file, idx) => {
12
12
  const commit = commitMap.get(fullPaths[idx]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openinc/parse-server-opendash",
3
- "version": "3.21.0",
3
+ "version": "3.22.0",
4
4
  "description": "Parse Server Cloud Code for open.INC Stack.",
5
5
  "packageManager": "pnpm@10.15.0",
6
6
  "keywords": [