@topogram/cli 0.3.34
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.
- package/ARCHITECTURE.md +67 -0
- package/CHANGELOG.md +240 -0
- package/README.md +223 -0
- package/package.json +51 -0
- package/src/adoption/index.js +3 -0
- package/src/adoption/plan.js +702 -0
- package/src/adoption/reporting.js +464 -0
- package/src/adoption/review-groups.js +313 -0
- package/src/agent-ops/query-builders.js +5012 -0
- package/src/archive/archive.js +141 -0
- package/src/archive/compact.js +26 -0
- package/src/archive/jsonl.js +70 -0
- package/src/archive/resolver-bridge.js +82 -0
- package/src/archive/schema.js +87 -0
- package/src/archive/unarchive.js +108 -0
- package/src/catalog.js +752 -0
- package/src/cli/catalog-alias.js +166 -0
- package/src/cli.js +9738 -0
- package/src/component-behavior.js +173 -0
- package/src/example-implementation.js +91 -0
- package/src/format.js +19 -0
- package/src/generator/adapters.d.ts +4 -0
- package/src/generator/adapters.js +325 -0
- package/src/generator/api.d.ts +1 -0
- package/src/generator/api.js +1196 -0
- package/src/generator/check.js +355 -0
- package/src/generator/component-conformance.js +767 -0
- package/src/generator/components.js +39 -0
- package/src/generator/context/bundle.js +291 -0
- package/src/generator/context/diff.js +256 -0
- package/src/generator/context/digest.js +182 -0
- package/src/generator/context/domain-coverage.js +94 -0
- package/src/generator/context/domain-page.js +137 -0
- package/src/generator/context/index.js +42 -0
- package/src/generator/context/report.js +121 -0
- package/src/generator/context/shared.js +1397 -0
- package/src/generator/context/slice.js +703 -0
- package/src/generator/context/task-mode.js +466 -0
- package/src/generator/docs.js +327 -0
- package/src/generator/index.js +161 -0
- package/src/generator/native/parity-bundle.js +311 -0
- package/src/generator/output.js +300 -0
- package/src/generator/registry.js +482 -0
- package/src/generator/runtime/app-bundle.js +456 -0
- package/src/generator/runtime/bundle-shared.js +166 -0
- package/src/generator/runtime/compile-check.js +163 -0
- package/src/generator/runtime/deployment.js +287 -0
- package/src/generator/runtime/environment.js +635 -0
- package/src/generator/runtime/index.js +32 -0
- package/src/generator/runtime/runtime-check.js +554 -0
- package/src/generator/runtime/shared.js +515 -0
- package/src/generator/runtime/smoke.js +219 -0
- package/src/generator/schema.js +204 -0
- package/src/generator/sdlc/board.js +66 -0
- package/src/generator/sdlc/doc-page.js +53 -0
- package/src/generator/sdlc/index.js +23 -0
- package/src/generator/sdlc/release-notes.js +62 -0
- package/src/generator/sdlc/traceability-matrix.js +65 -0
- package/src/generator/shared.js +29 -0
- package/src/generator/surfaces/contracts.js +146 -0
- package/src/generator/surfaces/databases/contract.js +40 -0
- package/src/generator/surfaces/databases/index.js +84 -0
- package/src/generator/surfaces/databases/lifecycle-shared.d.ts +1 -0
- package/src/generator/surfaces/databases/lifecycle-shared.js +612 -0
- package/src/generator/surfaces/databases/migration-plan.js +281 -0
- package/src/generator/surfaces/databases/postgres/capabilities.js +14 -0
- package/src/generator/surfaces/databases/postgres/drizzle.js +99 -0
- package/src/generator/surfaces/databases/postgres/index.js +9 -0
- package/src/generator/surfaces/databases/postgres/lifecycle.js +16 -0
- package/src/generator/surfaces/databases/postgres/prisma.js +159 -0
- package/src/generator/surfaces/databases/postgres/sql-migration.js +102 -0
- package/src/generator/surfaces/databases/postgres/sql-schema.js +34 -0
- package/src/generator/surfaces/databases/shared.d.ts +1 -0
- package/src/generator/surfaces/databases/shared.js +350 -0
- package/src/generator/surfaces/databases/snapshot.js +96 -0
- package/src/generator/surfaces/databases/sqlite/capabilities.js +14 -0
- package/src/generator/surfaces/databases/sqlite/index.js +8 -0
- package/src/generator/surfaces/databases/sqlite/lifecycle.js +16 -0
- package/src/generator/surfaces/databases/sqlite/prisma.js +143 -0
- package/src/generator/surfaces/databases/sqlite/sql-migration.js +65 -0
- package/src/generator/surfaces/databases/sqlite/sql-schema.js +27 -0
- package/src/generator/surfaces/index.js +25 -0
- package/src/generator/surfaces/native/swiftui-app.js +38 -0
- package/src/generator/surfaces/native/swiftui-templates/Package.swift.txt +20 -0
- package/src/generator/surfaces/native/swiftui-templates/README.generated.md +26 -0
- package/src/generator/surfaces/native/swiftui-templates/runtime/DynamicScreens.swift +682 -0
- package/src/generator/surfaces/native/swiftui-templates/runtime/TodoAPIClient.swift +156 -0
- package/src/generator/surfaces/native/swiftui-templates/runtime/TodoSwiftUIApp.swift +44 -0
- package/src/generator/surfaces/native/swiftui-templates/runtime/Visibility.swift +183 -0
- package/src/generator/surfaces/services/express.d.ts +1 -0
- package/src/generator/surfaces/services/express.js +766 -0
- package/src/generator/surfaces/services/hono.d.ts +1 -0
- package/src/generator/surfaces/services/hono.js +204 -0
- package/src/generator/surfaces/services/index.js +42 -0
- package/src/generator/surfaces/services/persistence-wiring.js +240 -0
- package/src/generator/surfaces/services/runtime-helpers.js +631 -0
- package/src/generator/surfaces/services/server-contract.js +80 -0
- package/src/generator/surfaces/services/stateless.d.ts +1 -0
- package/src/generator/surfaces/services/stateless.js +97 -0
- package/src/generator/surfaces/shared.js +64 -0
- package/src/generator/surfaces/web/api-client.js +1 -0
- package/src/generator/surfaces/web/forms.js +1 -0
- package/src/generator/surfaces/web/index.d.ts +2 -0
- package/src/generator/surfaces/web/index.js +53 -0
- package/src/generator/surfaces/web/react-components.js +248 -0
- package/src/generator/surfaces/web/react.js +538 -0
- package/src/generator/surfaces/web/routes.js +1 -0
- package/src/generator/surfaces/web/screens.js +1 -0
- package/src/generator/surfaces/web/shared.js +369 -0
- package/src/generator/surfaces/web/sveltekit-actions.js +28 -0
- package/src/generator/surfaces/web/sveltekit-components.js +234 -0
- package/src/generator/surfaces/web/sveltekit.js +426 -0
- package/src/generator/surfaces/web/ui-web-contract.js +65 -0
- package/src/generator/surfaces/web/vanilla.js +239 -0
- package/src/generator/verification.js +84 -0
- package/src/generator.js +1 -0
- package/src/import/core/context.js +52 -0
- package/src/import/core/contracts.js +23 -0
- package/src/import/core/registry.js +81 -0
- package/src/import/core/runner.js +646 -0
- package/src/import/core/shared.js +910 -0
- package/src/import/enrichers/auth-session.js +18 -0
- package/src/import/enrichers/django-rest.js +226 -0
- package/src/import/enrichers/doc-linking.js +20 -0
- package/src/import/enrichers/rails-controllers.js +246 -0
- package/src/import/enrichers/rails-models.js +130 -0
- package/src/import/enrichers/workflow-target-state.js +10 -0
- package/src/import/extractors/api/aspnet-core.js +304 -0
- package/src/import/extractors/api/django-routes.js +318 -0
- package/src/import/extractors/api/express.js +154 -0
- package/src/import/extractors/api/fastify.js +371 -0
- package/src/import/extractors/api/flutter-dio.js +135 -0
- package/src/import/extractors/api/generic-route-fallback.js +90 -0
- package/src/import/extractors/api/graphql-code-first.js +565 -0
- package/src/import/extractors/api/graphql-sdl.js +309 -0
- package/src/import/extractors/api/jaxrs.js +303 -0
- package/src/import/extractors/api/micronaut.js +213 -0
- package/src/import/extractors/api/next-route.js +50 -0
- package/src/import/extractors/api/next-server-action.js +51 -0
- package/src/import/extractors/api/nextauth.js +52 -0
- package/src/import/extractors/api/openapi-code.js +242 -0
- package/src/import/extractors/api/openapi.js +232 -0
- package/src/import/extractors/api/rails-routes.js +230 -0
- package/src/import/extractors/api/react-native-repository.js +128 -0
- package/src/import/extractors/api/retrofit.js +103 -0
- package/src/import/extractors/api/spring-web.js +372 -0
- package/src/import/extractors/api/swift-webapi.js +116 -0
- package/src/import/extractors/api/trpc.js +212 -0
- package/src/import/extractors/db/django-models.js +232 -0
- package/src/import/extractors/db/dotnet-models.js +93 -0
- package/src/import/extractors/db/drizzle.js +242 -0
- package/src/import/extractors/db/ef-core.js +221 -0
- package/src/import/extractors/db/flutter-entities.js +120 -0
- package/src/import/extractors/db/jpa.js +120 -0
- package/src/import/extractors/db/liquibase.js +180 -0
- package/src/import/extractors/db/mybatis-xml.js +145 -0
- package/src/import/extractors/db/prisma.js +185 -0
- package/src/import/extractors/db/rails-schema.js +175 -0
- package/src/import/extractors/db/react-native-entities.js +95 -0
- package/src/import/extractors/db/room.js +193 -0
- package/src/import/extractors/db/snapshot.js +112 -0
- package/src/import/extractors/db/sql.js +180 -0
- package/src/import/extractors/db/swiftdata.js +137 -0
- package/src/import/extractors/ui/android-compose.js +230 -0
- package/src/import/extractors/ui/backend-only.js +70 -0
- package/src/import/extractors/ui/blazor.js +227 -0
- package/src/import/extractors/ui/flutter-screens.js +152 -0
- package/src/import/extractors/ui/maui-xaml.js +135 -0
- package/src/import/extractors/ui/next-app-router.js +83 -0
- package/src/import/extractors/ui/next-pages-router.js +141 -0
- package/src/import/extractors/ui/razor-pages.js +181 -0
- package/src/import/extractors/ui/react-native-screens.js +166 -0
- package/src/import/extractors/ui/react-router.js +139 -0
- package/src/import/extractors/ui/sveltekit.js +123 -0
- package/src/import/extractors/ui/swiftui.js +193 -0
- package/src/import/extractors/ui/uikit.js +175 -0
- package/src/import/extractors/verification/generic.js +290 -0
- package/src/import/extractors/workflows/generic.js +137 -0
- package/src/import/index.js +7 -0
- package/src/import/provenance.js +158 -0
- package/src/new-project.js +2107 -0
- package/src/parser.js +439 -0
- package/src/policy/review-boundaries.js +165 -0
- package/src/project-config.js +535 -0
- package/src/proofs/backend-parity.js +19 -0
- package/src/proofs/contract-audit.js +220 -0
- package/src/proofs/ios-parity.js +7 -0
- package/src/proofs/issues-parity.js +10 -0
- package/src/proofs/web-parity.js +50 -0
- package/src/realization/api/build-api-realization.js +5 -0
- package/src/realization/api/index.js +1 -0
- package/src/realization/backend/build-backend-runtime-realization.js +82 -0
- package/src/realization/backend/index.d.ts +1 -0
- package/src/realization/backend/index.js +4 -0
- package/src/realization/db/build-db-realization.js +17 -0
- package/src/realization/db/index.js +3 -0
- package/src/realization/db/migration-plan.js +5 -0
- package/src/realization/db/snapshot.js +5 -0
- package/src/realization/ui/build-ui-shared-realization.js +305 -0
- package/src/realization/ui/build-web-realization.js +189 -0
- package/src/realization/ui/index.js +2 -0
- package/src/reconcile/docs.js +280 -0
- package/src/reconcile/index.js +3 -0
- package/src/reconcile/journeys.js +441 -0
- package/src/resolver/docs.js +1 -0
- package/src/resolver/enrich/acceptance-criterion.js +14 -0
- package/src/resolver/enrich/bug.js +12 -0
- package/src/resolver/enrich/component.js +2 -0
- package/src/resolver/enrich/index.js +1 -0
- package/src/resolver/enrich/pitch.js +18 -0
- package/src/resolver/enrich/requirement.js +20 -0
- package/src/resolver/enrich/task.js +16 -0
- package/src/resolver/expressions.js +1 -0
- package/src/resolver/index.js +2422 -0
- package/src/resolver/normalize.js +1 -0
- package/src/resolver.js +1 -0
- package/src/sdlc/adopt.js +65 -0
- package/src/sdlc/check.js +86 -0
- package/src/sdlc/dod/acceptance-criterion.js +22 -0
- package/src/sdlc/dod/bug.js +26 -0
- package/src/sdlc/dod/document.js +23 -0
- package/src/sdlc/dod/index.js +25 -0
- package/src/sdlc/dod/pitch.js +23 -0
- package/src/sdlc/dod/requirement.js +34 -0
- package/src/sdlc/dod/task.js +39 -0
- package/src/sdlc/explain.js +116 -0
- package/src/sdlc/history.js +80 -0
- package/src/sdlc/paths.js +11 -0
- package/src/sdlc/release.js +106 -0
- package/src/sdlc/scaffold.js +89 -0
- package/src/sdlc/status-filter.js +54 -0
- package/src/sdlc/transition.js +112 -0
- package/src/sdlc/transitions/acceptance-criterion.js +28 -0
- package/src/sdlc/transitions/bug.js +31 -0
- package/src/sdlc/transitions/document.js +29 -0
- package/src/sdlc/transitions/index.js +56 -0
- package/src/sdlc/transitions/pitch.js +34 -0
- package/src/sdlc/transitions/requirement.js +31 -0
- package/src/sdlc/transitions/task.js +34 -0
- package/src/template-trust.js +597 -0
- package/src/validator/expressions.js +1 -0
- package/src/validator/index.js +3424 -0
- package/src/validator/kinds.js +346 -0
- package/src/validator/per-kind/acceptance-criterion.js +91 -0
- package/src/validator/per-kind/bug.js +77 -0
- package/src/validator/per-kind/component.js +274 -0
- package/src/validator/per-kind/domain.js +205 -0
- package/src/validator/per-kind/pitch.js +101 -0
- package/src/validator/per-kind/requirement.js +75 -0
- package/src/validator/per-kind/task.js +96 -0
- package/src/validator/registry.js +1 -0
- package/src/validator/utils.js +12 -0
- package/src/validator.js +1 -0
- package/src/workflows.js +7597 -0
- package/src/workspace-docs.js +265 -0
- package/template-helpers/react.js +5 -0
- package/template-helpers/sveltekit.js +5 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { findImportFiles, inferRouteAuthHint, inferRouteCapabilityId, inferRouteQueryParams, makeCandidateRecord, normalizeOpenApiPath, relativeTo } from "../../core/shared.js";
|
|
2
|
+
|
|
3
|
+
function extractHandlerContext(text, handlerName) {
|
|
4
|
+
if (!handlerName) return "";
|
|
5
|
+
const escaped = handlerName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6
|
+
for (const pattern of [
|
|
7
|
+
new RegExp(`function\\s+${escaped}\\s*\\([^)]*\\)\\s*\\{([\\s\\S]{0,1200}?)\\n\\}`, "m"),
|
|
8
|
+
new RegExp(`const\\s+${escaped}\\s*=\\s*(?:async\\s*)?\\([^)]*\\)\\s*=>\\s*\\{([\\s\\S]{0,1200}?)\\n\\}`, "m")
|
|
9
|
+
]) {
|
|
10
|
+
const match = text.match(pattern);
|
|
11
|
+
if (match) return match[1];
|
|
12
|
+
}
|
|
13
|
+
return "";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function inferServerRoutes(context) {
|
|
17
|
+
const routes = [];
|
|
18
|
+
const routeFiles = findImportFiles(
|
|
19
|
+
context.paths,
|
|
20
|
+
(filePath) =>
|
|
21
|
+
/\.(ts|tsx|js|jsx|mjs|cjs)$/.test(filePath) &&
|
|
22
|
+
/(server|api|routes|src)/i.test(filePath)
|
|
23
|
+
);
|
|
24
|
+
for (const filePath of routeFiles) {
|
|
25
|
+
const text = context.helpers.readTextIfExists(filePath) || "";
|
|
26
|
+
for (const match of text.matchAll(/\.(get|post|put|patch|delete)\(\s*["'`]([^"'`]+)["'`]\s*,([\s\S]*?)\)\s*;?/gi)) {
|
|
27
|
+
const handlerTokens = [...match[3].matchAll(/\b([A-Za-z_][A-Za-z0-9_]*)\b/g)].map((entry) => entry[1]);
|
|
28
|
+
const handlerHint = handlerTokens.length > 0 ? handlerTokens[handlerTokens.length - 1] : null;
|
|
29
|
+
const handlerContext = handlerHint ? extractHandlerContext(text, handlerHint) : "";
|
|
30
|
+
routes.push({
|
|
31
|
+
file: filePath,
|
|
32
|
+
method: match[1].toUpperCase(),
|
|
33
|
+
path: match[2],
|
|
34
|
+
handler_hint: handlerHint,
|
|
35
|
+
path_params: [...normalizeOpenApiPath(match[2]).matchAll(/\{([^}]+)\}/g)].map((entry) => entry[1]),
|
|
36
|
+
query_params: inferRouteQueryParams(handlerContext),
|
|
37
|
+
auth_hint: inferRouteAuthHint(match[3], handlerContext)
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return routes;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const genericRouteFallbackExtractor = {
|
|
45
|
+
id: "api.generic-route-fallback",
|
|
46
|
+
track: "api",
|
|
47
|
+
detect(context) {
|
|
48
|
+
const routes = inferServerRoutes(context);
|
|
49
|
+
return {
|
|
50
|
+
score: routes.length > 0 ? 35 : 0,
|
|
51
|
+
reasons: routes.length > 0 ? ["Found generic server route handlers"] : []
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
extract(context) {
|
|
55
|
+
const routes = inferServerRoutes(context);
|
|
56
|
+
const findings = [];
|
|
57
|
+
const candidates = { capabilities: [], routes: [], stacks: [] };
|
|
58
|
+
if (routes.length > 0) {
|
|
59
|
+
findings.push({
|
|
60
|
+
kind: "route_inventory",
|
|
61
|
+
files: [...new Set(routes.map((route) => relativeTo(context.paths.repoRoot, route.file)))],
|
|
62
|
+
route_count: routes.length
|
|
63
|
+
});
|
|
64
|
+
candidates.capabilities.push(...routes.map((route) => makeCandidateRecord({
|
|
65
|
+
kind: "capability",
|
|
66
|
+
idHint: inferRouteCapabilityId(route),
|
|
67
|
+
label: `${route.method} ${route.path}`,
|
|
68
|
+
confidence: "medium",
|
|
69
|
+
sourceKind: "route_code",
|
|
70
|
+
provenance: `${relativeTo(context.paths.repoRoot, route.file)}#${route.method} ${route.path}`,
|
|
71
|
+
endpoint: { method: route.method, path: normalizeOpenApiPath(route.path) },
|
|
72
|
+
path_params: (route.path_params || []).map((name) => ({ name, required: true, type: null })),
|
|
73
|
+
query_params: (route.query_params || []).map((name) => ({ name, required: false, type: null })),
|
|
74
|
+
header_params: [],
|
|
75
|
+
input_fields: [],
|
|
76
|
+
output_fields: [],
|
|
77
|
+
auth_hint: route.auth_hint || "unknown",
|
|
78
|
+
track: "api"
|
|
79
|
+
})));
|
|
80
|
+
candidates.routes.push(...routes.map((route) => ({
|
|
81
|
+
path: normalizeOpenApiPath(route.path),
|
|
82
|
+
method: route.method,
|
|
83
|
+
confidence: "medium",
|
|
84
|
+
source_kind: "route_code",
|
|
85
|
+
provenance: `${relativeTo(context.paths.repoRoot, route.file)}#${route.method} ${route.path}`
|
|
86
|
+
})));
|
|
87
|
+
}
|
|
88
|
+
return { findings, candidates };
|
|
89
|
+
}
|
|
90
|
+
};
|
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
import {
|
|
2
|
+
canonicalCandidateTerm,
|
|
3
|
+
dedupeCandidateRecords,
|
|
4
|
+
findImportFiles,
|
|
5
|
+
idHintify,
|
|
6
|
+
makeCandidateRecord,
|
|
7
|
+
pluralizeCandidateTerm,
|
|
8
|
+
readTextIfExists,
|
|
9
|
+
relativeTo,
|
|
10
|
+
titleCase
|
|
11
|
+
} from "../../core/shared.js";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
|
|
14
|
+
function stripComments(text) {
|
|
15
|
+
return String(text || "")
|
|
16
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
17
|
+
.replace(/\/\/.*$/gm, "");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function findClassBlocks(text) {
|
|
21
|
+
const blocks = [];
|
|
22
|
+
const source = String(text || "");
|
|
23
|
+
let index = 0;
|
|
24
|
+
while (index < source.length) {
|
|
25
|
+
const classIndex = source.indexOf("class ", index);
|
|
26
|
+
if (classIndex === -1) break;
|
|
27
|
+
const headerStart = source.lastIndexOf("@", classIndex);
|
|
28
|
+
const scanStart = headerStart >= 0 && source.slice(headerStart, classIndex).includes("@") ? headerStart : classIndex;
|
|
29
|
+
const headerMatch = source.slice(classIndex).match(/^class\s+([A-Za-z_][A-Za-z0-9_]*)/);
|
|
30
|
+
if (!headerMatch) {
|
|
31
|
+
index = classIndex + 5;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const name = headerMatch[1];
|
|
35
|
+
const braceIndex = source.indexOf("{", classIndex);
|
|
36
|
+
if (braceIndex === -1) break;
|
|
37
|
+
let depth = 1;
|
|
38
|
+
let end = braceIndex + 1;
|
|
39
|
+
while (end < source.length && depth > 0) {
|
|
40
|
+
const char = source[end];
|
|
41
|
+
if (char === "{") depth += 1;
|
|
42
|
+
if (char === "}") depth -= 1;
|
|
43
|
+
end += 1;
|
|
44
|
+
}
|
|
45
|
+
blocks.push({
|
|
46
|
+
text: source.slice(scanStart, end),
|
|
47
|
+
header: source.slice(scanStart, braceIndex),
|
|
48
|
+
body: source.slice(braceIndex + 1, end - 1),
|
|
49
|
+
name
|
|
50
|
+
});
|
|
51
|
+
index = end;
|
|
52
|
+
}
|
|
53
|
+
return blocks;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function extractBalancedSegment(source, openIndex, openChar = "(", closeChar = ")") {
|
|
57
|
+
if (openIndex < 0 || source[openIndex] !== openChar) return null;
|
|
58
|
+
let depth = 1;
|
|
59
|
+
let index = openIndex + 1;
|
|
60
|
+
let inString = false;
|
|
61
|
+
let quote = null;
|
|
62
|
+
while (index < source.length && depth > 0) {
|
|
63
|
+
const char = source[index];
|
|
64
|
+
const prev = source[index - 1];
|
|
65
|
+
if (inString) {
|
|
66
|
+
if (char === quote && prev !== "\\") {
|
|
67
|
+
inString = false;
|
|
68
|
+
quote = null;
|
|
69
|
+
}
|
|
70
|
+
index += 1;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (char === "'" || char === '"' || char === "`") {
|
|
74
|
+
inString = true;
|
|
75
|
+
quote = char;
|
|
76
|
+
index += 1;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (char === openChar) depth += 1;
|
|
80
|
+
if (char === closeChar) depth -= 1;
|
|
81
|
+
index += 1;
|
|
82
|
+
}
|
|
83
|
+
return depth === 0 ? source.slice(openIndex + 1, index - 1) : null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function findCallBodies(text, callPrefix) {
|
|
87
|
+
const bodies = [];
|
|
88
|
+
let start = 0;
|
|
89
|
+
while (start < text.length) {
|
|
90
|
+
const idx = text.indexOf(callPrefix, start);
|
|
91
|
+
if (idx === -1) break;
|
|
92
|
+
const openIndex = text.indexOf("(", idx + callPrefix.length - 1);
|
|
93
|
+
if (openIndex === -1) break;
|
|
94
|
+
const body = extractBalancedSegment(text, openIndex, "(", ")");
|
|
95
|
+
if (body != null) {
|
|
96
|
+
bodies.push(body);
|
|
97
|
+
start = openIndex + body.length + 2;
|
|
98
|
+
} else {
|
|
99
|
+
start = idx + callPrefix.length;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return bodies;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function extractReturnedObjectBody(segment) {
|
|
106
|
+
const fieldsIdx = segment.indexOf("fields:");
|
|
107
|
+
if (fieldsIdx === -1) return null;
|
|
108
|
+
const objectStart = segment.indexOf("{", fieldsIdx);
|
|
109
|
+
if (objectStart === -1) return null;
|
|
110
|
+
return extractBalancedSegment(segment, objectStart, "{", "}");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function parseDecoratorClassFields(classBlock) {
|
|
114
|
+
const fields = [];
|
|
115
|
+
const body = stripComments(classBlock.body);
|
|
116
|
+
const pattern = /@Field\s*\(([\s\S]*?)\)\s*([\r\n\s]*)?([A-Za-z_][A-Za-z0-9_]*)\??\s*:/g;
|
|
117
|
+
for (const match of body.matchAll(pattern)) {
|
|
118
|
+
const decoratorArgs = match[1] || "";
|
|
119
|
+
const fieldName = match[3];
|
|
120
|
+
let typeName = null;
|
|
121
|
+
const arrowType = decoratorArgs.match(/=>\s*\[?\s*([A-Za-z_][A-Za-z0-9_]*)/);
|
|
122
|
+
if (arrowType) typeName = arrowType[1];
|
|
123
|
+
fields.push({
|
|
124
|
+
name: fieldName,
|
|
125
|
+
typeName
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
return fields;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function parseNestTypes(files) {
|
|
132
|
+
const inputTypes = new Map();
|
|
133
|
+
const objectTypes = new Map();
|
|
134
|
+
for (const filePath of files) {
|
|
135
|
+
const text = readTextIfExists(filePath) || "";
|
|
136
|
+
for (const classBlock of findClassBlocks(text)) {
|
|
137
|
+
if (/@InputType\s*\(/.test(classBlock.header)) {
|
|
138
|
+
inputTypes.set(classBlock.name, parseDecoratorClassFields(classBlock));
|
|
139
|
+
}
|
|
140
|
+
if (/@ObjectType\s*\(/.test(classBlock.header)) {
|
|
141
|
+
objectTypes.set(classBlock.name, parseDecoratorClassFields(classBlock));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return { inputTypes, objectTypes };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function parsePothosTypeName(fragment) {
|
|
149
|
+
const typeMatch =
|
|
150
|
+
String(fragment || "").match(/type\s*:\s*\[\s*([A-Za-z_][A-Za-z0-9_]*)\s*\]/) ||
|
|
151
|
+
String(fragment || "").match(/type\s*:\s*['"]([A-Za-z_][A-Za-z0-9_]*)['"]/) ||
|
|
152
|
+
String(fragment || "").match(/type\s*:\s*([A-Za-z_][A-Za-z0-9_]*)/);
|
|
153
|
+
return typeMatch ? typeMatch[1] : null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function parsePothosTypes(files) {
|
|
157
|
+
const inputTypes = new Map();
|
|
158
|
+
const objectTypes = new Map();
|
|
159
|
+
for (const filePath of files) {
|
|
160
|
+
const text = readTextIfExists(filePath) || "";
|
|
161
|
+
for (const body of findCallBodies(text, "builder.prismaObject")) {
|
|
162
|
+
const nameMatch = body.match(/^\s*['"]([A-Za-z_][A-Za-z0-9_]*)['"]/);
|
|
163
|
+
if (!nameMatch) continue;
|
|
164
|
+
const objectBody = extractReturnedObjectBody(body);
|
|
165
|
+
if (!objectBody) continue;
|
|
166
|
+
const fields = [];
|
|
167
|
+
for (const match of objectBody.matchAll(/([A-Za-z_][A-Za-z0-9_]*)\s*:\s*t\.(?:expose[A-Za-z_]+|relation)\s*\(/g)) {
|
|
168
|
+
fields.push({ name: match[1], typeName: null });
|
|
169
|
+
}
|
|
170
|
+
objectTypes.set(nameMatch[1], fields);
|
|
171
|
+
}
|
|
172
|
+
for (const body of findCallBodies(text, "builder.inputType")) {
|
|
173
|
+
const nameMatch = body.match(/^\s*['"]([A-Za-z_][A-Za-z0-9_]*)['"]/);
|
|
174
|
+
if (!nameMatch) continue;
|
|
175
|
+
const objectBody = extractReturnedObjectBody(body);
|
|
176
|
+
if (!objectBody) continue;
|
|
177
|
+
const fields = [];
|
|
178
|
+
for (const match of objectBody.matchAll(/([A-Za-z_][A-Za-z0-9_]*)\s*:\s*t\.(string|int|id|field)\s*\(([\s\S]*?)\)/g)) {
|
|
179
|
+
const fieldName = match[1];
|
|
180
|
+
const fieldKind = match[2];
|
|
181
|
+
const decoratorArgs = match[3] || "";
|
|
182
|
+
fields.push({
|
|
183
|
+
name: fieldName,
|
|
184
|
+
typeName: fieldKind === "field" ? parsePothosTypeName(decoratorArgs) : null
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
inputTypes.set(nameMatch[1], fields);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return { inputTypes, objectTypes };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function parseNexusTypes(files) {
|
|
194
|
+
const inputTypes = new Map();
|
|
195
|
+
const objectTypes = new Map();
|
|
196
|
+
const operationBlocks = [];
|
|
197
|
+
for (const filePath of files) {
|
|
198
|
+
const text = readTextIfExists(filePath) || "";
|
|
199
|
+
for (const body of findCallBodies(text, "objectType")) {
|
|
200
|
+
const nameMatch = body.match(/name\s*:\s*['"]([A-Za-z_][A-Za-z0-9_]*)['"]/);
|
|
201
|
+
if (!nameMatch) continue;
|
|
202
|
+
const typeName = nameMatch[1];
|
|
203
|
+
const definitionMatch = body.match(/definition\s*\(\s*t\s*\)\s*\{/);
|
|
204
|
+
if (!definitionMatch) continue;
|
|
205
|
+
const definitionStart = body.indexOf("{", definitionMatch.index);
|
|
206
|
+
const definitionBody = extractBalancedSegment(body, definitionStart, "{", "}");
|
|
207
|
+
if (!definitionBody) continue;
|
|
208
|
+
if (typeName === "Query" || typeName === "Mutation") {
|
|
209
|
+
operationBlocks.push({ typeName, body: definitionBody, filePath });
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
const fields = [];
|
|
213
|
+
for (const line of definitionBody.matchAll(/t(?:\.[A-Za-z_]+)*\.(?:field|string|int|boolean)\s*\(\s*['"]([A-Za-z_][A-Za-z0-9_]*)['"](?:\s*,\s*\{([\s\S]*?)\})?/g)) {
|
|
214
|
+
const fieldName = line[1];
|
|
215
|
+
const options = line[2] || "";
|
|
216
|
+
const fieldType = parsePothosTypeName(options);
|
|
217
|
+
fields.push({ name: fieldName, typeName: fieldType });
|
|
218
|
+
}
|
|
219
|
+
objectTypes.set(typeName, fields);
|
|
220
|
+
}
|
|
221
|
+
for (const body of findCallBodies(text, "inputObjectType")) {
|
|
222
|
+
const nameMatch = body.match(/name\s*:\s*['"]([A-Za-z_][A-Za-z0-9_]*)['"]/);
|
|
223
|
+
if (!nameMatch) continue;
|
|
224
|
+
const typeName = nameMatch[1];
|
|
225
|
+
const definitionMatch = body.match(/definition\s*\(\s*t\s*\)\s*\{/);
|
|
226
|
+
if (!definitionMatch) continue;
|
|
227
|
+
const definitionStart = body.indexOf("{", definitionMatch.index);
|
|
228
|
+
const definitionBody = extractBalancedSegment(body, definitionStart, "{", "}");
|
|
229
|
+
if (!definitionBody) continue;
|
|
230
|
+
const fields = [];
|
|
231
|
+
for (const line of definitionBody.matchAll(/t(?:\.[A-Za-z_]+)*\.(field|string|int|boolean)\s*\(\s*['"]([A-Za-z_][A-Za-z0-9_]*)['"](?:\s*,\s*\{([\s\S]*?)\})?/g)) {
|
|
232
|
+
const fieldKind = line[1];
|
|
233
|
+
const fieldName = line[2];
|
|
234
|
+
const options = line[3] || "";
|
|
235
|
+
fields.push({
|
|
236
|
+
name: fieldName,
|
|
237
|
+
typeName: fieldKind === "field" ? parsePothosTypeName(options) : null
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
inputTypes.set(typeName, fields);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return { inputTypes, objectTypes, operationBlocks };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function flattenInputFields(typeName, inputTypes, seen = new Set()) {
|
|
247
|
+
const normalized = String(typeName || "").replace(/[^\w]/g, "");
|
|
248
|
+
if (!normalized || seen.has(normalized)) return [];
|
|
249
|
+
const fields = inputTypes.get(normalized);
|
|
250
|
+
if (!fields) return [];
|
|
251
|
+
seen.add(normalized);
|
|
252
|
+
const names = [];
|
|
253
|
+
for (const field of fields) {
|
|
254
|
+
const nested = flattenInputFields(field.typeName, inputTypes, seen);
|
|
255
|
+
if (nested.length > 0) {
|
|
256
|
+
names.push(...nested);
|
|
257
|
+
} else {
|
|
258
|
+
names.push(field.name);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
seen.delete(normalized);
|
|
262
|
+
return [...new Set(names)].sort();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function outputFieldsForType(typeName, objectTypes) {
|
|
266
|
+
const normalized = String(typeName || "").replace(/[^\w]/g, "");
|
|
267
|
+
return [...new Set((objectTypes.get(normalized) || []).map((field) => field.name))].sort();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function inferEntityId(operationName, returnType) {
|
|
271
|
+
const normalizedReturn = String(returnType || "").replace(/[^\w]/g, "");
|
|
272
|
+
if (normalizedReturn && !["String", "Int", "Float", "Boolean", "ID", "Date"].includes(normalizedReturn)) {
|
|
273
|
+
return `entity_${idHintify(canonicalCandidateTerm(normalizedReturn))}`;
|
|
274
|
+
}
|
|
275
|
+
const suffixMatch = String(operationName || "").match(/([A-Z][A-Za-z0-9_]*)$/);
|
|
276
|
+
return `entity_${idHintify(canonicalCandidateTerm(suffixMatch ? suffixMatch[1] : operationName || "item"))}`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function inferCapabilityId(operationName, returnType, rootType) {
|
|
280
|
+
const entityStem = inferEntityId(operationName, returnType).replace(/^entity_/, "");
|
|
281
|
+
const normalizedName = idHintify(operationName);
|
|
282
|
+
if (rootType === "Query" && canonicalCandidateTerm(operationName) === canonicalCandidateTerm(entityStem)) {
|
|
283
|
+
return `cap_get_${entityStem}`;
|
|
284
|
+
}
|
|
285
|
+
if (/(signup|register)/i.test(operationName)) return `cap_register_${entityStem}`;
|
|
286
|
+
if (/(signin|sign_in|login|authenticate)/i.test(normalizedName)) return `cap_sign_in_${entityStem}`;
|
|
287
|
+
if (/(delete|remove)/i.test(operationName)) return `cap_delete_${entityStem}`;
|
|
288
|
+
if (/(togglepublish|publish)/i.test(normalizedName)) return `cap_publish_${entityStem}`;
|
|
289
|
+
if (/increment.*view.*count/i.test(normalizedName)) return `cap_update_${entityStem}_view_count`;
|
|
290
|
+
if (/(create|add|new)/i.test(operationName)) return `cap_create_${entityStem}`;
|
|
291
|
+
if (rootType === "Query" && (/^(all|list|feed|drafts)/i.test(operationName) || /^\[/.test(String(returnType || "")))) {
|
|
292
|
+
return `cap_list_${pluralizeCandidateTerm(entityStem)}`;
|
|
293
|
+
}
|
|
294
|
+
if (rootType === "Query" && /(byid|get|find|detail)/i.test(operationName)) {
|
|
295
|
+
return `cap_get_${entityStem}`;
|
|
296
|
+
}
|
|
297
|
+
if (rootType === "Mutation") return `cap_update_${entityStem}`;
|
|
298
|
+
return `${rootType === "Mutation" ? "cap_update_" : "cap_get_"}${entityStem}_${normalizedName}`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function inferTargetState(operationName, capabilityId) {
|
|
302
|
+
if (/(signup|register)/i.test(operationName)) return "registered";
|
|
303
|
+
if (/(signin|login|authenticate)/i.test(operationName)) return "authenticated";
|
|
304
|
+
if (/(togglepublish|publish)/i.test(operationName)) return "published";
|
|
305
|
+
if (/delete/i.test(operationName) || capabilityId.startsWith("cap_delete_")) return "deleted";
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function inferGraphqlEndpoint(files) {
|
|
310
|
+
for (const filePath of files) {
|
|
311
|
+
const text = readTextIfExists(filePath) || "";
|
|
312
|
+
const explicitPath = text.match(/\bpath\s*:\s*["'`]([^"'`]+)["'`]/);
|
|
313
|
+
if (explicitPath) return explicitPath[1];
|
|
314
|
+
if (/GraphQLModule\.forRoot/.test(text) || /createYoga|ApolloServer/.test(text)) {
|
|
315
|
+
return "/graphql";
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return "/graphql";
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function parseResolverOperations(filePath, text, inputTypes, objectTypes, endpointPath) {
|
|
322
|
+
const operations = [];
|
|
323
|
+
const source = String(text || "");
|
|
324
|
+
const pattern = /@(Query|Mutation)\s*\(([\s\S]*?)\)\s*(?:async\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*\(([\s\S]*?)\)\s*[:{]/g;
|
|
325
|
+
for (const match of source.matchAll(pattern)) {
|
|
326
|
+
const rootType = match[1];
|
|
327
|
+
const decoratorArgs = match[2] || "";
|
|
328
|
+
const methodName = match[3];
|
|
329
|
+
const params = match[4] || "";
|
|
330
|
+
const returnTypeMatch = decoratorArgs.match(/=>\s*(\[[A-Za-z_][A-Za-z0-9_]*\]|[A-Za-z_][A-Za-z0-9_]*)/);
|
|
331
|
+
const returnType = returnTypeMatch ? returnTypeMatch[1] : null;
|
|
332
|
+
const inputFields = [];
|
|
333
|
+
for (const paramMatch of params.matchAll(/@Args\(\s*['"`]([^'"`]+)['"`][^)]*\)\s*([A-Za-z_][A-Za-z0-9_]*)\s*:\s*([^,\n)]+)/g)) {
|
|
334
|
+
const argName = paramMatch[1];
|
|
335
|
+
const paramType = paramMatch[3].trim();
|
|
336
|
+
const nested = flattenInputFields(paramType, inputTypes);
|
|
337
|
+
if (nested.length > 0) {
|
|
338
|
+
inputFields.push(...nested);
|
|
339
|
+
} else {
|
|
340
|
+
inputFields.push(argName);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
const capabilityId = inferCapabilityId(methodName, returnType, rootType);
|
|
344
|
+
operations.push({
|
|
345
|
+
rootType,
|
|
346
|
+
methodName,
|
|
347
|
+
capabilityId,
|
|
348
|
+
inputFields: [...new Set(inputFields)].sort(),
|
|
349
|
+
outputFields: outputFieldsForType(returnType, objectTypes),
|
|
350
|
+
entityId: inferEntityId(methodName, returnType),
|
|
351
|
+
targetState: inferTargetState(methodName, capabilityId),
|
|
352
|
+
provenance: `${relativeTo(path.dirname(path.dirname(filePath)), filePath).replace(/^src\//, `${relativeTo(path.dirname(path.dirname(filePath)), path.dirname(path.dirname(filePath)))}`)}`,
|
|
353
|
+
filePath,
|
|
354
|
+
endpointPath
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
return operations;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function parsePothosOperations(filePath, text, inputTypes, objectTypes, endpointPath) {
|
|
361
|
+
const operations = [];
|
|
362
|
+
const source = String(text || "");
|
|
363
|
+
for (const rootType of ["queryField", "mutationField"]) {
|
|
364
|
+
for (const body of findCallBodies(source, `builder.${rootType}`)) {
|
|
365
|
+
const nameMatch = body.match(/^\s*['"]([A-Za-z_][A-Za-z0-9_]*)['"]/);
|
|
366
|
+
if (!nameMatch) continue;
|
|
367
|
+
const methodName = nameMatch[1];
|
|
368
|
+
const prismaFieldMatch = body.match(/t\.prismaField\s*\(\s*\{/);
|
|
369
|
+
if (!prismaFieldMatch) continue;
|
|
370
|
+
const prismaFieldStart = body.indexOf("{", prismaFieldMatch.index);
|
|
371
|
+
const prismaFieldBody = extractBalancedSegment(body, prismaFieldStart, "{", "}");
|
|
372
|
+
if (!prismaFieldBody) continue;
|
|
373
|
+
const returnTypeMatch =
|
|
374
|
+
prismaFieldBody.match(/type\s*:\s*\[\s*['"]?([A-Za-z_][A-Za-z0-9_]*)['"]?\s*\]/) ||
|
|
375
|
+
prismaFieldBody.match(/type\s*:\s*['"]([A-Za-z_][A-Za-z0-9_]*)['"]/) ||
|
|
376
|
+
prismaFieldBody.match(/type\s*:\s*([A-Za-z_][A-Za-z0-9_]*)/);
|
|
377
|
+
const returnType = returnTypeMatch ? returnTypeMatch[0].includes("[") ? `[${returnTypeMatch[1]}]` : returnTypeMatch[1] : null;
|
|
378
|
+
const inputFields = [];
|
|
379
|
+
const argsMatch = prismaFieldBody.match(/args\s*:\s*\{/);
|
|
380
|
+
if (argsMatch) {
|
|
381
|
+
const argsStart = prismaFieldBody.indexOf("{", argsMatch.index);
|
|
382
|
+
const argsBody = extractBalancedSegment(prismaFieldBody, argsStart, "{", "}");
|
|
383
|
+
for (const argMatch of String(argsBody || "").matchAll(/([A-Za-z_][A-Za-z0-9_]*)\s*:\s*t\.arg(?:\.[A-Za-z_]+)?\s*\(([\s\S]*?)\)/g)) {
|
|
384
|
+
const argName = argMatch[1];
|
|
385
|
+
const argBody = argMatch[2] || "";
|
|
386
|
+
const nestedType = parsePothosTypeName(argBody);
|
|
387
|
+
const nestedFields = flattenInputFields(nestedType, inputTypes);
|
|
388
|
+
if (nestedFields.length > 0) {
|
|
389
|
+
inputFields.push(...nestedFields);
|
|
390
|
+
} else {
|
|
391
|
+
inputFields.push(argName);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
const capabilityId = inferCapabilityId(methodName, returnType, rootType === "mutationField" ? "Mutation" : "Query");
|
|
396
|
+
operations.push({
|
|
397
|
+
rootType: rootType === "mutationField" ? "Mutation" : "Query",
|
|
398
|
+
methodName,
|
|
399
|
+
capabilityId,
|
|
400
|
+
inputFields: [...new Set(inputFields)].sort(),
|
|
401
|
+
outputFields: outputFieldsForType(returnType, objectTypes),
|
|
402
|
+
entityId: inferEntityId(methodName, returnType),
|
|
403
|
+
targetState: inferTargetState(methodName, capabilityId),
|
|
404
|
+
filePath,
|
|
405
|
+
endpointPath
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return operations;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function parseNexusArgFields(argExpression, inputTypes) {
|
|
413
|
+
const fields = [];
|
|
414
|
+
for (const match of String(argExpression || "").matchAll(/([A-Za-z_][A-Za-z0-9_]*)\s*:\s*([\s\S]*?)(?:,|$)/g)) {
|
|
415
|
+
const argName = match[1];
|
|
416
|
+
const argBody = match[2] || "";
|
|
417
|
+
const nestedType = parsePothosTypeName(argBody);
|
|
418
|
+
const nestedFields = flattenInputFields(nestedType, inputTypes);
|
|
419
|
+
if (nestedFields.length > 0) {
|
|
420
|
+
fields.push(...nestedFields);
|
|
421
|
+
} else {
|
|
422
|
+
fields.push(argName);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return [...new Set(fields)].sort();
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function parseNexusOperations(operationBlocks, inputTypes, objectTypes, endpointPath) {
|
|
429
|
+
const operations = [];
|
|
430
|
+
for (const block of operationBlocks) {
|
|
431
|
+
const rootType = block.typeName;
|
|
432
|
+
const source = block.body;
|
|
433
|
+
for (const match of source.matchAll(/t(?:\.[A-Za-z_]+)*\.field\s*\(\s*['"]([A-Za-z_][A-Za-z0-9_]*)['"]\s*,\s*\{/g)) {
|
|
434
|
+
const methodName = match[1];
|
|
435
|
+
const bodyStart = source.indexOf("{", match.index + match[0].lastIndexOf("{"));
|
|
436
|
+
const fieldBody = extractBalancedSegment(source, bodyStart, "{", "}");
|
|
437
|
+
if (!fieldBody) continue;
|
|
438
|
+
const typeMatch = parsePothosTypeName(fieldBody);
|
|
439
|
+
const argsMatch = fieldBody.match(/args\s*:\s*\{/);
|
|
440
|
+
const inputFields = [];
|
|
441
|
+
if (argsMatch) {
|
|
442
|
+
const argsStart = fieldBody.indexOf("{", argsMatch.index);
|
|
443
|
+
const argsBody = extractBalancedSegment(fieldBody, argsStart, "{", "}");
|
|
444
|
+
inputFields.push(...parseNexusArgFields(argsBody, inputTypes));
|
|
445
|
+
}
|
|
446
|
+
const capabilityId = inferCapabilityId(methodName, typeMatch, rootType);
|
|
447
|
+
operations.push({
|
|
448
|
+
rootType,
|
|
449
|
+
methodName,
|
|
450
|
+
capabilityId,
|
|
451
|
+
inputFields: [...new Set(inputFields)].sort(),
|
|
452
|
+
outputFields: outputFieldsForType(typeMatch, objectTypes),
|
|
453
|
+
entityId: inferEntityId(methodName, typeMatch),
|
|
454
|
+
targetState: inferTargetState(methodName, capabilityId),
|
|
455
|
+
filePath: block.filePath,
|
|
456
|
+
endpointPath
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return operations;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export const graphQlCodeFirstExtractor = {
|
|
464
|
+
id: "api.graphql-code-first",
|
|
465
|
+
track: "api",
|
|
466
|
+
detect(context) {
|
|
467
|
+
const files = findImportFiles(context.paths, (filePath) => /(\/src\/.+|\/pages\/api\/.+)\.(ts|tsx|js|jsx)$/i.test(filePath));
|
|
468
|
+
const hasNestResolvers = files.some((filePath) => {
|
|
469
|
+
const text = readTextIfExists(filePath) || "";
|
|
470
|
+
return /@nestjs\/graphql/.test(text) && /@(Query|Mutation)\s*\(/.test(text);
|
|
471
|
+
});
|
|
472
|
+
const hasPothosSource = files.some((filePath) => {
|
|
473
|
+
const text = readTextIfExists(filePath) || "";
|
|
474
|
+
return /@pothos\/core/.test(text) && /builder\.(queryField|mutationField)\s*\(/.test(text);
|
|
475
|
+
});
|
|
476
|
+
const hasNexusSource = files.some((filePath) => {
|
|
477
|
+
const text = readTextIfExists(filePath) || "";
|
|
478
|
+
return /from ['"]nexus['"]/.test(text) && /objectType\s*\(\s*\{/.test(text) && /name\s*:\s*['"](Query|Mutation)['"]/.test(text);
|
|
479
|
+
});
|
|
480
|
+
return {
|
|
481
|
+
score: hasNestResolvers ? 86 : hasPothosSource ? 84 : hasNexusSource ? 82 : 0,
|
|
482
|
+
reasons: hasNestResolvers
|
|
483
|
+
? ["Found source-only Nest GraphQL resolvers"]
|
|
484
|
+
: hasPothosSource
|
|
485
|
+
? ["Found source-only Pothos GraphQL schema definitions"]
|
|
486
|
+
: hasNexusSource
|
|
487
|
+
? ["Found source-only Nexus GraphQL schema definitions"]
|
|
488
|
+
: []
|
|
489
|
+
};
|
|
490
|
+
},
|
|
491
|
+
extract(context) {
|
|
492
|
+
const files = findImportFiles(context.paths, (filePath) => /(\/src\/.+|\/pages\/api\/.+)\.(ts|tsx|js|jsx)$/i.test(filePath)).filter((filePath) => !/\.test\./i.test(filePath));
|
|
493
|
+
const nestTypes = parseNestTypes(files);
|
|
494
|
+
const pothosTypes = parsePothosTypes(files);
|
|
495
|
+
const nexusTypes = parseNexusTypes(files);
|
|
496
|
+
const inputTypes = new Map([...nestTypes.inputTypes.entries(), ...pothosTypes.inputTypes.entries(), ...nexusTypes.inputTypes.entries()]);
|
|
497
|
+
const objectTypes = new Map([...nestTypes.objectTypes.entries(), ...pothosTypes.objectTypes.entries(), ...nexusTypes.objectTypes.entries()]);
|
|
498
|
+
const endpointPath = inferGraphqlEndpoint(files);
|
|
499
|
+
const operations = [];
|
|
500
|
+
for (const filePath of files) {
|
|
501
|
+
const text = readTextIfExists(filePath) || "";
|
|
502
|
+
if (/@nestjs\/graphql/.test(text) && /@(Query|Mutation)\s*\(/.test(text)) {
|
|
503
|
+
operations.push(...parseResolverOperations(filePath, text, inputTypes, objectTypes, endpointPath));
|
|
504
|
+
}
|
|
505
|
+
if (/@pothos\/core/.test(text) && /builder\.(queryField|mutationField)\s*\(/.test(text)) {
|
|
506
|
+
operations.push(...parsePothosOperations(filePath, text, inputTypes, objectTypes, endpointPath));
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
operations.push(...parseNexusOperations(nexusTypes.operationBlocks, inputTypes, objectTypes, endpointPath));
|
|
510
|
+
const findings = [];
|
|
511
|
+
const candidates = { capabilities: [], routes: [], stacks: [] };
|
|
512
|
+
for (const operation of operations) {
|
|
513
|
+
const provenance = `${relativeTo(context.paths.repoRoot, operation.filePath)}#${operation.rootType}.${operation.methodName}`;
|
|
514
|
+
candidates.capabilities.push(
|
|
515
|
+
makeCandidateRecord({
|
|
516
|
+
kind: "capability",
|
|
517
|
+
idHint: operation.capabilityId,
|
|
518
|
+
label: titleCase(operation.capabilityId.replace(/^cap_/, "")),
|
|
519
|
+
confidence: "high",
|
|
520
|
+
sourceKind: "route_code",
|
|
521
|
+
provenance,
|
|
522
|
+
endpoint: {
|
|
523
|
+
method: operation.rootType === "Mutation" ? "POST" : "GET",
|
|
524
|
+
path: endpointPath
|
|
525
|
+
},
|
|
526
|
+
path_params: [],
|
|
527
|
+
query_params: [],
|
|
528
|
+
header_params: [],
|
|
529
|
+
input_fields: operation.inputFields,
|
|
530
|
+
output_fields: operation.outputFields,
|
|
531
|
+
auth_hint: "public",
|
|
532
|
+
entity_id: operation.entityId,
|
|
533
|
+
graphql_operation: {
|
|
534
|
+
root_type: operation.rootType,
|
|
535
|
+
field: operation.methodName
|
|
536
|
+
},
|
|
537
|
+
target_state: operation.targetState,
|
|
538
|
+
track: "api"
|
|
539
|
+
})
|
|
540
|
+
);
|
|
541
|
+
candidates.routes.push({
|
|
542
|
+
path: endpointPath,
|
|
543
|
+
method: operation.rootType === "Mutation" ? "POST" : "GET",
|
|
544
|
+
confidence: "high",
|
|
545
|
+
source_kind: "route_code",
|
|
546
|
+
provenance
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
if (operations.length > 0) {
|
|
550
|
+
findings.push({
|
|
551
|
+
kind: "graphql_code_first_operations",
|
|
552
|
+
files: [...new Set(operations.map((entry) => relativeTo(context.paths.repoRoot, entry.filePath)))],
|
|
553
|
+
operation_count: operations.length
|
|
554
|
+
});
|
|
555
|
+
candidates.stacks.push("graphql_code_first");
|
|
556
|
+
}
|
|
557
|
+
candidates.capabilities = dedupeCandidateRecords(candidates.capabilities, (record) => record.id_hint);
|
|
558
|
+
candidates.routes = dedupeCandidateRecords(
|
|
559
|
+
candidates.routes.map((route) => ({ ...route, id_hint: `${route.method}_${route.path}` })),
|
|
560
|
+
(record) => `${record.method}:${record.path}:${record.source_kind}`
|
|
561
|
+
).map(({ id_hint, ...route }) => route);
|
|
562
|
+
candidates.stacks = [...new Set(candidates.stacks)].sort();
|
|
563
|
+
return { findings, candidates };
|
|
564
|
+
}
|
|
565
|
+
};
|