mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
build(server): lint:boundaries counts the RPC and MCP layers as domains and sees getRepository calls
src/nest-rpc, src/nest-mcp and src/mcp import domains and are imported by them, but the domain map only knew src/nest, so their edges dropped out of domainCycles and their repository injections out of foreignRepositories. Each layer now counts as one domain. The repository rule reads the syntax tree for @InjectRepository(X) and for getRepository(X) on an EntityManager, so a comment no longer counts and a foreign read through the EntityManager no longer passes. The baseline takes the cycles and the repository reaches the wider view finds; settings -> Users goes, it was a comment.
This commit is contained in:
@@ -2,27 +2,106 @@
|
||||
"fileCycles": [],
|
||||
"domainCycles": [
|
||||
"accommodations -> assignments",
|
||||
"accommodations -> nest-rpc",
|
||||
"accommodations -> places",
|
||||
"assignments -> days",
|
||||
"assignments -> journey",
|
||||
"assignments -> nest-rpc",
|
||||
"atlas -> nest-rpc",
|
||||
"auth -> budget",
|
||||
"auth -> files",
|
||||
"auth -> plugins",
|
||||
"budget -> nest-rpc",
|
||||
"calendar -> days",
|
||||
"calendar -> reservations",
|
||||
"categories -> nest-rpc",
|
||||
"collab -> files",
|
||||
"collab -> nest-rpc",
|
||||
"collections -> nest-rpc",
|
||||
"collections -> places",
|
||||
"day-notes -> nest-rpc",
|
||||
"days -> accommodations",
|
||||
"days -> assignments",
|
||||
"days -> nest-rpc",
|
||||
"days -> places",
|
||||
"files -> nest-rpc",
|
||||
"geo -> maps",
|
||||
"integrations -> assignments",
|
||||
"integrations -> atlas",
|
||||
"integrations -> journey",
|
||||
"integrations -> places",
|
||||
"integrations -> reservations",
|
||||
"journey -> atlas",
|
||||
"journey -> files",
|
||||
"journey -> nest-rpc",
|
||||
"maps -> geo",
|
||||
"memories -> photos",
|
||||
"nest-rpc -> plugins",
|
||||
"packing -> nest-rpc",
|
||||
"photos -> memories",
|
||||
"places -> accommodations",
|
||||
"places -> assignments",
|
||||
"places -> atlas",
|
||||
"places -> journey",
|
||||
"places -> nest-rpc",
|
||||
"plugins -> accommodations",
|
||||
"plugins -> assignments",
|
||||
"plugins -> atlas",
|
||||
"plugins -> budget",
|
||||
"plugins -> categories",
|
||||
"plugins -> collab",
|
||||
"plugins -> collections",
|
||||
"plugins -> day-notes",
|
||||
"plugins -> days",
|
||||
"plugins -> files",
|
||||
"plugins -> journey",
|
||||
"plugins -> nest-rpc",
|
||||
"plugins -> packing",
|
||||
"plugins -> places",
|
||||
"plugins -> reservations",
|
||||
"plugins -> tags",
|
||||
"plugins -> todo",
|
||||
"plugins -> trips",
|
||||
"plugins -> vacay",
|
||||
"plugins -> weather",
|
||||
"reservations -> accommodations",
|
||||
"reservations -> assignments",
|
||||
"reservations -> budget",
|
||||
"reservations -> days",
|
||||
"reservations -> integrations",
|
||||
"reservations -> nest-rpc",
|
||||
"tags -> nest-rpc",
|
||||
"todo -> nest-rpc",
|
||||
"trip-members -> auth",
|
||||
"trip-members -> budget",
|
||||
"trip-members -> trips",
|
||||
"trip-read-model -> accommodations",
|
||||
"trip-read-model -> budget",
|
||||
"trip-read-model -> collab",
|
||||
"trip-read-model -> days",
|
||||
"trip-read-model -> files",
|
||||
"trip-read-model -> packing",
|
||||
"trip-read-model -> places",
|
||||
"trip-read-model -> reservations",
|
||||
"trip-read-model -> todo",
|
||||
"trip-read-model -> trip-members",
|
||||
"trip-read-model -> trips",
|
||||
"trips -> accommodations",
|
||||
"trips -> budget",
|
||||
"trips -> calendar",
|
||||
"trips -> collab",
|
||||
"trips -> days",
|
||||
"trips -> files",
|
||||
"trips -> nest-rpc",
|
||||
"trips -> packing",
|
||||
"trips -> places",
|
||||
"trips -> reservations",
|
||||
"trips -> todo",
|
||||
"trips -> trip-members",
|
||||
"trips -> trip-read-model"
|
||||
"trips -> trip-read-model",
|
||||
"trips -> vacay",
|
||||
"vacay -> nest-rpc",
|
||||
"weather -> nest-rpc"
|
||||
],
|
||||
"sharedImportsDomain": [
|
||||
"nest/common/idempotency-cleanup.job.ts -> nest/audit/audit-log.logger.ts",
|
||||
@@ -61,6 +140,7 @@
|
||||
"journey -> storage/storage-upload.factory.ts",
|
||||
"maps -> geo/nominatim.client.ts",
|
||||
"maps -> settings/instance-api-keys.ts",
|
||||
"nest-rpc -> plugins/protocol/envelope.ts",
|
||||
"place-enrichment -> maps/maps.helpers.ts",
|
||||
"place-enrichment -> maps/trek-places.client.ts",
|
||||
"places -> atlas/atlas-geo.ts",
|
||||
@@ -144,8 +224,10 @@
|
||||
"auth -> TripMembers (owned by trip-membership)",
|
||||
"auth -> Trips (owned by trips)",
|
||||
"auth-core -> AppSettings (owned by settings)",
|
||||
"auth-core -> UserSessions (owned by sessions)",
|
||||
"auth-core -> Users (owned by auth)",
|
||||
"auth-core -> WebauthnCredentials (owned by auth)",
|
||||
"backup -> UserSessions (owned by sessions)",
|
||||
"booking-import -> Days (owned by days)",
|
||||
"booking-import -> Reservations (owned by reservations)",
|
||||
"budget -> Places (owned by places)",
|
||||
@@ -164,6 +246,7 @@
|
||||
"collections -> TripMembers (owned by trip-membership)",
|
||||
"collections -> Trips (owned by trips)",
|
||||
"collections -> Users (owned by auth)",
|
||||
"common -> Users (owned by auth)",
|
||||
"day-notes -> Days (owned by days)",
|
||||
"days -> DayAccommodations (owned by accommodations)",
|
||||
"days -> DayAssignments (owned by assignments)",
|
||||
@@ -184,6 +267,7 @@
|
||||
"files -> DayAssignments (owned by assignments)",
|
||||
"files -> Places (owned by places)",
|
||||
"files -> Reservations (owned by reservations)",
|
||||
"files -> UserSessions (owned by sessions)",
|
||||
"files -> Users (owned by auth)",
|
||||
"google-quota -> AppSettings (owned by settings)",
|
||||
"integrations -> AppSettings (owned by settings)",
|
||||
@@ -210,6 +294,8 @@
|
||||
"memories -> TripPhotos (owned by photos)",
|
||||
"memories -> Trips (owned by trips)",
|
||||
"memories -> Users (owned by auth)",
|
||||
"nest-rpc -> Trips (owned by trips)",
|
||||
"nest-rpc -> Users (owned by auth)",
|
||||
"notifications -> AppSettings (owned by settings)",
|
||||
"notifications -> Settings (owned by settings)",
|
||||
"notifications -> TodoItems (owned by todo)",
|
||||
@@ -235,6 +321,10 @@
|
||||
"places -> Tags (owned by tags)",
|
||||
"places -> TripMembers (owned by trip-membership)",
|
||||
"places -> Trips (owned by trips)",
|
||||
"platform -> Photos (owned by photos)",
|
||||
"platform -> ShareTokens (owned by share)",
|
||||
"platform -> UserSessions (owned by sessions)",
|
||||
"platform -> Users (owned by auth)",
|
||||
"plugins -> DayAccommodations (owned by accommodations)",
|
||||
"plugins -> Days (owned by days)",
|
||||
"plugins -> JourneyEntries (owned by journey)",
|
||||
@@ -243,6 +333,7 @@
|
||||
"plugins -> Reservations (owned by reservations)",
|
||||
"plugins -> Settings (owned by settings)",
|
||||
"plugins -> Trips (owned by trips)",
|
||||
"plugins -> UserSessions (owned by sessions)",
|
||||
"plugins -> Users (owned by auth)",
|
||||
"public-api -> BucketList (owned by atlas)",
|
||||
"public-api -> DayNotes (owned by day-notes)",
|
||||
@@ -274,7 +365,6 @@
|
||||
"roadtrip -> Users (owned by auth)",
|
||||
"route-usage -> AppSettings (owned by settings)",
|
||||
"school-holidays -> VacayHolidayCalendars (owned by vacay)",
|
||||
"settings -> Users (owned by auth)",
|
||||
"share -> BudgetItems (owned by budget)",
|
||||
"share -> Categories (owned by categories)",
|
||||
"share -> CollabMessages (owned by collab)",
|
||||
@@ -299,6 +389,30 @@
|
||||
"trip-members -> Users (owned by auth)",
|
||||
"trip-membership -> Trips (owned by trips)",
|
||||
"trip-read-model -> Trips (owned by trips)",
|
||||
"trips -> AssignmentParticipants (owned by assignments)",
|
||||
"trips -> BudgetCategoryOrder (owned by budget)",
|
||||
"trips -> BudgetItemMembers (owned by budget)",
|
||||
"trips -> BudgetItemPayers (owned by budget)",
|
||||
"trips -> BudgetItems (owned by budget)",
|
||||
"trips -> DayAccommodations (owned by accommodations)",
|
||||
"trips -> DayAssignments (owned by assignments)",
|
||||
"trips -> DayNotes (owned by day-notes)",
|
||||
"trips -> Days (owned by days)",
|
||||
"trips -> JourneyEntries (owned by journey)",
|
||||
"trips -> PackingBags (owned by packing)",
|
||||
"trips -> PackingItems (owned by packing)",
|
||||
"trips -> Places (owned by places)",
|
||||
"trips -> Reservations (owned by reservations)",
|
||||
"trips -> RoadtripDayBoundaries (owned by roadtrip)",
|
||||
"trips -> RoadtripDayTracks (owned by roadtrip)",
|
||||
"trips -> RoadtripPreferences (owned by roadtrip)",
|
||||
"trips -> RoadtripVias (owned by roadtrip)",
|
||||
"trips -> Tags (owned by tags)",
|
||||
"trips -> TodoItems (owned by todo)",
|
||||
"trips -> TourWaypoints (owned by tours)",
|
||||
"trips -> Tours (owned by tours)",
|
||||
"trips -> TripMembers (owned by trip-membership)",
|
||||
"trips -> Users (owned by auth)",
|
||||
"unsplash -> AppSettings (owned by settings)",
|
||||
"unsplash -> Users (owned by auth)",
|
||||
"vacay -> SchoolHolidayRegions (owned by school-holidays)"
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
* So are the imports of src/db/entities/: MikroORM relations
|
||||
* point both ways by design and are read through `() => X`
|
||||
* thunks after every file has loaded.
|
||||
* domainCycles the same, between src/nest/<domain> folders: domain A
|
||||
* domainCycles the same, between src/nest/<domain> folders (and the
|
||||
* layers in LAYERS, src/nest-rpc, src/nest-mcp and src/mcp,
|
||||
* each counted as one domain): domain A
|
||||
* imports from B while B (directly or through others)
|
||||
* imports from A. Keyed by the domain pair, so a further
|
||||
* import along a dependency that already exists is not new;
|
||||
@@ -38,8 +40,10 @@
|
||||
* dbImportsNest a file under src/db/ importing from src/nest/. The data
|
||||
* layer sits below the domains, never the other way round.
|
||||
* foreignRepositories
|
||||
* a class in src/nest/<A>/ injecting (@InjectRepository) the
|
||||
* repository of an entity another domain owns. Ownership is
|
||||
* a class in src/nest/<A>/ (or one of LAYERS) reaching the
|
||||
* repository of an entity another domain owns, through
|
||||
* @InjectRepository(X) or a getRepository(X) call on an
|
||||
* EntityManager. Ownership is
|
||||
* the map in scripts/repository-owners.json (entity class ->
|
||||
* owning domain folder); an injected entity missing from it
|
||||
* stops the run. Keyed by domain and entity, so a second
|
||||
@@ -282,7 +286,22 @@ function cycleEdges(pairs) {
|
||||
return out;
|
||||
}
|
||||
|
||||
const domainOf = (file) => file.match(/^nest\/([^/]+)\//)?.[1] ?? null;
|
||||
/**
|
||||
* Layers outside src/nest that work like a domain of their own: the plugin RPC
|
||||
* kit, the MCP decorator layer and the process-wide MCP state. They import
|
||||
* domains and domains import them, so they take part in domainCycles and
|
||||
* foreignRepositories under their folder name; moving a file out of src/nest
|
||||
* does not take its edges out of the count.
|
||||
*/
|
||||
export const LAYERS = ['nest-rpc', 'nest-mcp', 'mcp'];
|
||||
|
||||
/** The domain a src/-relative path belongs to: nest/<domain>/..., one of LAYERS, or null. */
|
||||
export const domainOf = (file) => {
|
||||
const nest = file.match(/^nest\/([^/]+)\//)?.[1];
|
||||
if (nest) return nest;
|
||||
const top = file.split('/')[0];
|
||||
return file.includes('/') && LAYERS.includes(top) ? top : null;
|
||||
};
|
||||
|
||||
/** The orchestrators no provider may import: its own domain's main service, and MapsService everywhere. */
|
||||
function isOrchestratorOf(providerFile, target) {
|
||||
@@ -293,6 +312,8 @@ function isOrchestratorOf(providerFile, target) {
|
||||
const isProviderFile = (file) => /^nest\/[^/]+\/(?:.+\/)?providers\//.test(file);
|
||||
|
||||
export function isPublic(target) {
|
||||
// A layer outside src/nest is a toolkit every domain is written against.
|
||||
if (!target.startsWith('nest/')) return true;
|
||||
const inner = target.slice('nest/'.length);
|
||||
const domain = inner.split('/')[0];
|
||||
if (SHARED_DOMAINS.has(domain)) return true;
|
||||
@@ -300,18 +321,44 @@ export function isPublic(target) {
|
||||
return PUBLIC_SUFFIXES.some((s) => target.endsWith(s));
|
||||
}
|
||||
|
||||
/** Every @InjectRepository(Entity) under src/nest: { domain, entity, file }. */
|
||||
/**
|
||||
* The entity classes a file reaches a repository of: `@InjectRepository(X)`
|
||||
* and `<anything>.getRepository(X)`, read from the syntax tree so a comment or
|
||||
* a string naming them does not count.
|
||||
*/
|
||||
export function repositoryEntitiesOf(text, fileName = 'file.ts') {
|
||||
const source = ts.createSourceFile(fileName, text, ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
|
||||
const found = [];
|
||||
const visit = (node) => {
|
||||
if (ts.isCallExpression(node) && node.arguments.length > 0 && ts.isIdentifier(node.arguments[0])) {
|
||||
const callee = node.expression;
|
||||
const name = ts.isIdentifier(callee)
|
||||
? callee.text
|
||||
: ts.isPropertyAccessExpression(callee)
|
||||
? callee.name.text
|
||||
: null;
|
||||
if (name === 'InjectRepository' || name === 'getRepository') found.push(node.arguments[0].text);
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
visit(source);
|
||||
return found;
|
||||
}
|
||||
|
||||
/** Every repository a class under src/nest or one of LAYERS reaches: { domain, entity, file }. */
|
||||
export function collectRepositoryInjections(serverDir) {
|
||||
const src = join(serverDir, 'src');
|
||||
const nest = join(src, 'nest');
|
||||
if (!existsSync(nest)) return [];
|
||||
const found = [];
|
||||
for (const file of walk(nest)) {
|
||||
const rel = relative(src, file).split(sep).join('/');
|
||||
const domain = domainOf(rel);
|
||||
if (!domain) continue;
|
||||
for (const m of readFileSync(file, 'utf8').matchAll(/@InjectRepository\(\s*([A-Za-z_$][\w$]*)\s*\)/g)) {
|
||||
found.push({ domain, entity: m[1], file: rel });
|
||||
for (const top of ['nest', ...LAYERS]) {
|
||||
const dir = join(src, top);
|
||||
if (!existsSync(dir)) continue;
|
||||
for (const file of walk(dir)) {
|
||||
const rel = relative(src, file).split(sep).join('/');
|
||||
const domain = domainOf(rel);
|
||||
if (!domain) continue;
|
||||
for (const entity of repositoryEntitiesOf(readFileSync(file, 'utf8'), file)) {
|
||||
found.push({ domain, entity, file: rel });
|
||||
}
|
||||
}
|
||||
}
|
||||
return found;
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"PasswordResetTokens": "auth",
|
||||
"PhotoProviderFields": "addons",
|
||||
"PhotoProviders": "addons",
|
||||
"Photos": "photos",
|
||||
"PlaceDetailsCache": "maps",
|
||||
"PlaceRatings": "places",
|
||||
"PlaceRegions": "atlas",
|
||||
@@ -89,9 +90,11 @@
|
||||
"Reservations": "reservations",
|
||||
"RoadtripDayBoundaries": "roadtrip",
|
||||
"RoadtripDayTracks": "roadtrip",
|
||||
"RoadtripPreferences": "roadtrip",
|
||||
"RoadtripPreferencesEntity": "roadtrip",
|
||||
"RoadtripVias": "roadtrip",
|
||||
"RouteUsageDaily": "route-usage",
|
||||
"SchedulerLeases": "scheduling",
|
||||
"SchoolHolidayCountries": "school-holidays",
|
||||
"SchoolHolidayPeriods": "school-holidays",
|
||||
"SchoolHolidayRegions": "school-holidays",
|
||||
|
||||
Reference in New Issue
Block a user