mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 15:36:33 +00:00
build(server): lint:boundaries holds foreign repository injections to a baseline
A domain injecting the repository of a table another domain owns couples the two below the service layer, and nothing counted it. The new foreignRepositories rule reads every @InjectRepository under src/nest, looks the entity up in scripts/repository-owners.json (an injected entity without an owner stops the run) and holds each domain/entity pair to the baseline, which only shrinks: 209 pairs today.
This commit is contained in:
+1
-1
@@ -32,7 +32,7 @@ npm run lint:query-api # MikroORM QueryBuilder calls in the repositories may
|
||||
npm run contracts:dto-open # open shapes in the createZodDto schemas may only shrink (runs in the unit tests)
|
||||
```
|
||||
|
||||
**`lint:boundaries`** (`scripts/import-boundaries.mjs`) parses every import under `src/` and holds five lists in `scripts/import-boundaries-baseline.json`: load-time file cycles (type-only and lazy imports, and the MikroORM entity relations, do not count), dependencies between two `nest/<domain>` folders that already depend on each other, shared-kernel files (`nest/common`, `nest/database`, `nest/app-config`) importing a domain, a domain importing another domain's internals, and `src/db` importing `src/nest`. A domain's public surface is its `*.module`, `*.service`, `*.guard`, `*.decorator`, `*.types`, `*.interface`, `*.contract`, `*.constants` and `*.logger` files plus the short `PUBLIC_FILES` list in the script; anything else is an internal. A new entry fails the check, and so does a baseline entry that no longer occurs, so the fix lands with `npm run lint:boundaries -- --update`, which drops such entries and never adds one.
|
||||
**`lint:boundaries`** (`scripts/import-boundaries.mjs`) parses every import under `src/` and holds six lists in `scripts/import-boundaries-baseline.json`: load-time file cycles (type-only and lazy imports, and the MikroORM entity relations, do not count), dependencies between two `nest/<domain>` folders that already depend on each other, shared-kernel files (`nest/common`, `nest/database`, `nest/app-config`, `nest/auth-core`) importing a domain, a domain importing another domain's internals, `src/db` importing `src/nest`, and a domain injecting (`@InjectRepository`) the repository of an entity another domain owns (`foreignRepositories`; the owner of every injected entity is listed in `scripts/repository-owners.json`, and an entity missing there stops the run). For trip visibility, inject `TripAccessService` (trip-membership) instead of `TripsRepository`. A domain's public surface is its `*.module`, `*.service`, `*.guard`, `*.decorator`, `*.types`, `*.interface`, `*.contract`, `*.constants` and `*.logger` files plus the short `PUBLIC_FILES` list in the script; anything else is an internal. A new entry fails the check, and so does a baseline entry that no longer occurs, so the fix lands with `npm run lint:boundaries -- --update`, which drops such entries and never adds one.
|
||||
|
||||
**`contracts:dto-open`** (`tests/unit/contracts/dto-open-shapes.test.ts`, wrapped by `scripts/dto-open-shapes.mjs`) finds every `export class X extends createZodDto(...)` under `src/`, imports it and counts the open shapes (loose object, record of unknown, `z.unknown()`/`z.any()`, `z.custom()`) in the schema the Zod pipe validates, with shared's `contracts:open` walk. Each DTO class is held to its entry in `scripts/dto-open-shapes-baseline.json`, and a new one may hold none, whatever its schema is called and wherever it is declared. A `createZodDto` call that is not an exported class, or two DTO classes with one name, fail the check. After narrowing a schema, `npm run contracts:dto-open -- --update` lowers the baseline (it never raises or adds an entry). It needs `shared/` built.
|
||||
|
||||
|
||||
@@ -96,5 +96,216 @@
|
||||
"db/migrations/Migration20200101010300_encrypt_any_plaintext_immich_api_key_values.ts -> nest/common/crypto/apiKeyCrypto.ts",
|
||||
"db/orm.ts -> nest/database/request-context.ts",
|
||||
"db/reseat-booked-nights.ts -> nest/accommodations/night-seat.ts"
|
||||
],
|
||||
"foreignRepositories": [
|
||||
"accommodations -> BudgetItems (owned by budget)",
|
||||
"accommodations -> DayAssignments (owned by assignments)",
|
||||
"accommodations -> Days (owned by days)",
|
||||
"accommodations -> Places (owned by places)",
|
||||
"accommodations -> Reservations (owned by reservations)",
|
||||
"accommodations -> RoadtripVias (owned by roadtrip)",
|
||||
"addons -> AppSettings (owned by settings)",
|
||||
"addons -> Users (owned by auth)",
|
||||
"admin -> Addons (owned by addons)",
|
||||
"admin -> AppSettings (owned by settings)",
|
||||
"admin -> AuditLog (owned by audit)",
|
||||
"admin -> DocumentProviders (owned by doc-sync)",
|
||||
"admin -> McpTokens (owned by tokens)",
|
||||
"admin -> OauthTokens (owned by oauth)",
|
||||
"admin -> PhotoProviderFields (owned by addons)",
|
||||
"admin -> PhotoProviders (owned by addons)",
|
||||
"admin -> Places (owned by places)",
|
||||
"admin -> PushSubscriptions (owned by notifications)",
|
||||
"admin -> TripFiles (owned by files)",
|
||||
"admin -> Trips (owned by trips)",
|
||||
"admin -> Users (owned by auth)",
|
||||
"airports -> ReservationEndpoints (owned by reservations)",
|
||||
"airports -> Reservations (owned by reservations)",
|
||||
"assignments -> Days (owned by days)",
|
||||
"assignments -> Places (owned by places)",
|
||||
"assignments -> RoadtripVias (owned by roadtrip)",
|
||||
"assignments -> Tours (owned by tours)",
|
||||
"assignments -> TripMembers (owned by trip-membership)",
|
||||
"atlas -> AppSettings (owned by settings)",
|
||||
"atlas -> Places (owned by places)",
|
||||
"atlas -> ReservationEndpoints (owned by reservations)",
|
||||
"atlas -> Trips (owned by trips)",
|
||||
"audit -> Users (owned by auth)",
|
||||
"auth -> AppSettings (owned by settings)",
|
||||
"auth -> BudgetItems (owned by budget)",
|
||||
"auth -> BudgetSettlements (owned by budget)",
|
||||
"auth -> JourneyContributors (owned by journey)",
|
||||
"auth -> JourneyEntries (owned by journey)",
|
||||
"auth -> JourneyShareTokens (owned by journey)",
|
||||
"auth -> Journeys (owned by journey)",
|
||||
"auth -> McpTokens (owned by tokens)",
|
||||
"auth -> OauthTokens (owned by oauth)",
|
||||
"auth -> PluginUserErasureQueue (owned by plugins)",
|
||||
"auth -> Plugins (owned by plugins)",
|
||||
"auth -> PushSubscriptions (owned by notifications)",
|
||||
"auth -> ShareTokens (owned by share)",
|
||||
"auth -> TripMembers (owned by trip-membership)",
|
||||
"auth -> Trips (owned by trips)",
|
||||
"auth-core -> AppSettings (owned by settings)",
|
||||
"auth-core -> Users (owned by auth)",
|
||||
"auth-core -> WebauthnCredentials (owned by auth)",
|
||||
"booking-import -> Days (owned by days)",
|
||||
"booking-import -> Reservations (owned by reservations)",
|
||||
"budget -> Places (owned by places)",
|
||||
"budget -> Reservations (owned by reservations)",
|
||||
"budget -> TripMembers (owned by trip-membership)",
|
||||
"budget -> Trips (owned by trips)",
|
||||
"calendar -> DayNotes (owned by day-notes)",
|
||||
"calendar -> Days (owned by days)",
|
||||
"calendar -> Reservations (owned by reservations)",
|
||||
"calendar -> Trips (owned by trips)",
|
||||
"collab -> Trips (owned by trips)",
|
||||
"collections -> Categories (owned by categories)",
|
||||
"collections -> PlaceRatings (owned by places)",
|
||||
"collections -> Places (owned by places)",
|
||||
"collections -> Tags (owned by tags)",
|
||||
"collections -> TripMembers (owned by trip-membership)",
|
||||
"collections -> Trips (owned by trips)",
|
||||
"collections -> Users (owned by auth)",
|
||||
"day-notes -> Days (owned by days)",
|
||||
"days -> DayAccommodations (owned by accommodations)",
|
||||
"days -> DayAssignments (owned by assignments)",
|
||||
"days -> DayNotes (owned by day-notes)",
|
||||
"days -> ReservationEndpoints (owned by reservations)",
|
||||
"days -> Reservations (owned by reservations)",
|
||||
"days -> RoadtripDayBoundaries (owned by roadtrip)",
|
||||
"days -> RoadtripVias (owned by roadtrip)",
|
||||
"days -> Trips (owned by trips)",
|
||||
"doc-sync -> AppSettings (owned by settings)",
|
||||
"doc-sync -> FileLinks (owned by files)",
|
||||
"doc-sync -> TripFiles (owned by files)",
|
||||
"doc-sync -> Trips (owned by trips)",
|
||||
"feeds -> Trips (owned by trips)",
|
||||
"feeds -> Users (owned by auth)",
|
||||
"files -> AppSettings (owned by settings)",
|
||||
"files -> BudgetItems (owned by budget)",
|
||||
"files -> DayAssignments (owned by assignments)",
|
||||
"files -> Places (owned by places)",
|
||||
"files -> Reservations (owned by reservations)",
|
||||
"files -> Users (owned by auth)",
|
||||
"google-quota -> AppSettings (owned by settings)",
|
||||
"integrations -> AppSettings (owned by settings)",
|
||||
"integrations -> BucketList (owned by atlas)",
|
||||
"integrations -> Days (owned by days)",
|
||||
"integrations -> Places (owned by places)",
|
||||
"integrations -> ReservationEndpoints (owned by reservations)",
|
||||
"integrations -> Reservations (owned by reservations)",
|
||||
"integrations -> Trips (owned by trips)",
|
||||
"integrations -> Users (owned by auth)",
|
||||
"journey -> Places (owned by places)",
|
||||
"journey -> Trips (owned by trips)",
|
||||
"journey -> Users (owned by auth)",
|
||||
"llm-parse -> Addons (owned by addons)",
|
||||
"maps -> AppSettings (owned by settings)",
|
||||
"maps -> Places (owned by places)",
|
||||
"maps -> Users (owned by auth)",
|
||||
"mcp-shared -> Trips (owned by trips)",
|
||||
"mcp-shared -> Users (owned by auth)",
|
||||
"memories -> JourneyContributors (owned by journey)",
|
||||
"memories -> JourneyPhotos (owned by journey)",
|
||||
"memories -> Journeys (owned by journey)",
|
||||
"memories -> PhotoProviders (owned by addons)",
|
||||
"memories -> TrekPhotos (owned by photos)",
|
||||
"memories -> TripPhotos (owned by photos)",
|
||||
"memories -> Trips (owned by trips)",
|
||||
"memories -> Users (owned by auth)",
|
||||
"notifications -> AppSettings (owned by settings)",
|
||||
"notifications -> Settings (owned by settings)",
|
||||
"notifications -> TodoItems (owned by todo)",
|
||||
"notifications -> Trips (owned by trips)",
|
||||
"notifications -> Users (owned by auth)",
|
||||
"oidc -> AppSettings (owned by settings)",
|
||||
"oidc -> InviteTokens (owned by auth)",
|
||||
"oidc -> Users (owned by auth)",
|
||||
"packing -> TripMembers (owned by trip-membership)",
|
||||
"packing -> Trips (owned by trips)",
|
||||
"permissions -> AppSettings (owned by settings)",
|
||||
"permissions -> Trips (owned by trips)",
|
||||
"photos -> JourneyPhotos (owned by journey)",
|
||||
"place-enrichment -> AppSettings (owned by settings)",
|
||||
"place-enrichment -> PlaceDetailsCache (owned by maps)",
|
||||
"place-photos -> CollectionPlaces (owned by collections)",
|
||||
"place-photos -> Places (owned by places)",
|
||||
"place-shadow -> AppSettings (owned by settings)",
|
||||
"places -> BudgetItems (owned by budget)",
|
||||
"places -> Categories (owned by categories)",
|
||||
"places -> CollectionPlaces (owned by collections)",
|
||||
"places -> DayAssignments (owned by assignments)",
|
||||
"places -> Tags (owned by tags)",
|
||||
"places -> TripMembers (owned by trip-membership)",
|
||||
"places -> Trips (owned by trips)",
|
||||
"plugins -> DayAccommodations (owned by accommodations)",
|
||||
"plugins -> Days (owned by days)",
|
||||
"plugins -> JourneyEntries (owned by journey)",
|
||||
"plugins -> NotificationChannelPreferences (owned by notifications)",
|
||||
"plugins -> Places (owned by places)",
|
||||
"plugins -> Reservations (owned by reservations)",
|
||||
"plugins -> Settings (owned by settings)",
|
||||
"plugins -> Trips (owned by trips)",
|
||||
"plugins -> Users (owned by auth)",
|
||||
"public-api -> BucketList (owned by atlas)",
|
||||
"public-api -> DayNotes (owned by day-notes)",
|
||||
"public-api -> Days (owned by days)",
|
||||
"public-api -> Places (owned by places)",
|
||||
"public-api -> Reservations (owned by reservations)",
|
||||
"public-api -> Trips (owned by trips)",
|
||||
"query-helpers -> AssignmentParticipants (owned by assignments)",
|
||||
"query-helpers -> PlaceRatings (owned by places)",
|
||||
"query-helpers -> Tags (owned by tags)",
|
||||
"realtime -> AppSettings (owned by settings)",
|
||||
"realtime -> Users (owned by auth)",
|
||||
"reservations -> BudgetItems (owned by budget)",
|
||||
"reservations -> DayAccommodations (owned by accommodations)",
|
||||
"reservations -> DayAssignments (owned by assignments)",
|
||||
"reservations -> Days (owned by days)",
|
||||
"reservations -> Places (owned by places)",
|
||||
"reservations -> TripMembers (owned by trip-membership)",
|
||||
"reservations -> Trips (owned by trips)",
|
||||
"reservations -> Users (owned by auth)",
|
||||
"roadtrip -> DayAccommodations (owned by accommodations)",
|
||||
"roadtrip -> DayAssignments (owned by assignments)",
|
||||
"roadtrip -> Days (owned by days)",
|
||||
"roadtrip -> Places (owned by places)",
|
||||
"roadtrip -> Plugins (owned by plugins)",
|
||||
"roadtrip -> ReservationDayPositions (owned by reservations)",
|
||||
"roadtrip -> ReservationEndpoints (owned by reservations)",
|
||||
"roadtrip -> Reservations (owned by reservations)",
|
||||
"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)",
|
||||
"share -> DayAssignments (owned by assignments)",
|
||||
"share -> DayNotes (owned by day-notes)",
|
||||
"share -> Days (owned by days)",
|
||||
"share -> PackingItems (owned by packing)",
|
||||
"share -> Places (owned by places)",
|
||||
"share -> Reservations (owned by reservations)",
|
||||
"share -> Trips (owned by trips)",
|
||||
"storage -> AppSettings (owned by settings)",
|
||||
"system-notices -> AppSettings (owned by settings)",
|
||||
"system-notices -> Trips (owned by trips)",
|
||||
"system-notices -> Users (owned by auth)",
|
||||
"todo -> TripMembers (owned by trip-membership)",
|
||||
"tokens -> Users (owned by auth)",
|
||||
"tours -> Places (owned by places)",
|
||||
"transit -> AppSettings (owned by settings)",
|
||||
"transit -> Users (owned by auth)",
|
||||
"trip-members -> TripMembers (owned by trip-membership)",
|
||||
"trip-members -> Trips (owned by trips)",
|
||||
"trip-members -> Users (owned by auth)",
|
||||
"trip-membership -> Trips (owned by trips)",
|
||||
"trip-read-model -> Trips (owned by trips)",
|
||||
"unsplash -> AppSettings (owned by settings)",
|
||||
"unsplash -> Users (owned by auth)",
|
||||
"vacay -> SchoolHolidayRegions (owned by school-holidays)",
|
||||
"vacay -> Users (owned by auth)"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -37,6 +37,15 @@
|
||||
* importing domain, so moving a file inside A changes nothing.
|
||||
* 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
|
||||
* 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
|
||||
* injection of an entity the domain already reads is not new.
|
||||
* Asking the owner instead (a service such as
|
||||
* trip-membership's TripAccessService) is what shrinks it.
|
||||
*
|
||||
* Each rule's baseline lists today's violations. A violation not in it fails
|
||||
* the check, and so does an entry that no longer occurs, until --update drops
|
||||
@@ -56,7 +65,7 @@ import { fileURLToPath } from 'node:url';
|
||||
const require = createRequire(import.meta.url);
|
||||
const ts = require('typescript');
|
||||
|
||||
export const RULES = ['fileCycles', 'domainCycles', 'sharedImportsDomain', 'domainInternals', 'dbImportsNest'];
|
||||
export const RULES = ['fileCycles', 'domainCycles', 'sharedImportsDomain', 'domainInternals', 'dbImportsNest', 'foreignRepositories'];
|
||||
|
||||
/**
|
||||
* Folders under src/nest whose every file is shared infrastructure, open to all domains.
|
||||
@@ -271,6 +280,48 @@ export function isPublic(target) {
|
||||
return PUBLIC_SUFFIXES.some((s) => target.endsWith(s));
|
||||
}
|
||||
|
||||
/** Every @InjectRepository(Entity) under src/nest: { 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 });
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
/** scripts/repository-owners.json: entity class -> owning domain. Missing is an empty map. */
|
||||
export function readOwners(path) {
|
||||
if (!existsSync(path)) return {};
|
||||
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
||||
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||
throw new Error('scripts/repository-owners.json must map each entity class to its owning domain');
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/** foreignRepositories keys, plus the injected entities the owner map does not know. */
|
||||
export function analyseRepositories(injections, owners) {
|
||||
const foreign = new Set();
|
||||
const unowned = new Set();
|
||||
for (const { domain, entity, file } of injections) {
|
||||
const owner = owners[entity];
|
||||
if (typeof owner !== 'string') {
|
||||
unowned.add(`${entity} (${file})`);
|
||||
continue;
|
||||
}
|
||||
if (owner !== domain) foreign.add(`${domain} -> ${entity} (owned by ${owner})`);
|
||||
}
|
||||
return { foreignRepositories: [...foreign].sort(), unowned: [...unowned].sort() };
|
||||
}
|
||||
|
||||
/** The violations of every rule, each a sorted list of stable string keys. */
|
||||
export function analyse(edges) {
|
||||
const runtime = edges.filter((e) => e.runtime);
|
||||
@@ -301,6 +352,7 @@ export function analyse(edges) {
|
||||
sharedImportsDomain: sorted(sharedImportsDomain),
|
||||
domainInternals: sorted(domainInternals),
|
||||
dbImportsNest: sorted(dbImportsNest),
|
||||
foreignRepositories: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -347,6 +399,8 @@ const HINTS = {
|
||||
sharedImportsDomain:
|
||||
'The shared kernel is imported by every domain, so it may not import one. Move the piece into the kernel, or inject it from the domain.',
|
||||
dbImportsNest: 'src/db sits below src/nest. Move what the data layer needs into src/db (or src/utils) instead.',
|
||||
foreignRepositories:
|
||||
"This injects the repository of a table another domain owns. Ask the owner's service instead (e.g. TripAccessService for trip visibility), or move the code to the owning domain.",
|
||||
};
|
||||
|
||||
function main(argv) {
|
||||
@@ -362,6 +416,16 @@ function main(argv) {
|
||||
return 1;
|
||||
}
|
||||
const found = analyse(edges);
|
||||
const { foreignRepositories, unowned } = analyseRepositories(
|
||||
collectRepositoryInjections(serverDir),
|
||||
readOwners(join(serverDir, 'scripts', 'repository-owners.json')),
|
||||
);
|
||||
if (unowned.length) {
|
||||
// An entity without an owner is an injection the rule cannot judge.
|
||||
for (const u of unowned) console.error(`FAIL no owner for injected entity ${u}: add it to scripts/repository-owners.json`);
|
||||
return 1;
|
||||
}
|
||||
found.foreignRepositories = foreignRepositories;
|
||||
let baseline = readBaseline(baselinePath);
|
||||
if (update) {
|
||||
const now = Object.fromEntries(RULES.map((rule) => [rule, new Set(found[rule])]));
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
{
|
||||
"Addons": "addons",
|
||||
"AppSettings": "settings",
|
||||
"AssignmentParticipants": "assignments",
|
||||
"AuditLog": "audit",
|
||||
"BucketList": "atlas",
|
||||
"BudgetCategoryOrder": "budget",
|
||||
"BudgetItemMembers": "budget",
|
||||
"BudgetItemPayers": "budget",
|
||||
"BudgetItems": "budget",
|
||||
"BudgetSettlements": "budget",
|
||||
"Categories": "categories",
|
||||
"CollabLinks": "collab",
|
||||
"CollabMessageReactions": "collab",
|
||||
"CollabMessages": "collab",
|
||||
"CollabNotes": "collab",
|
||||
"CollabPollVotes": "collab",
|
||||
"CollabPolls": "collab",
|
||||
"CollectionLabels": "collections",
|
||||
"CollectionMembers": "collections",
|
||||
"CollectionPlaceRatings": "collections",
|
||||
"CollectionPlaces": "collections",
|
||||
"Collections": "collections",
|
||||
"DawarichConnections": "integrations",
|
||||
"DawarichVisitSuggestions": "integrations",
|
||||
"DayAccommodations": "accommodations",
|
||||
"DayAssignments": "assignments",
|
||||
"DayNotes": "day-notes",
|
||||
"Days": "days",
|
||||
"DocumentConnections": "doc-sync",
|
||||
"DocumentProviderFields": "doc-sync",
|
||||
"DocumentProviders": "doc-sync",
|
||||
"DocumentSyncItems": "doc-sync",
|
||||
"FileLinks": "files",
|
||||
"GoogleApiUsage": "google-quota",
|
||||
"GooglePlacePhotoMeta": "place-photos",
|
||||
"HiddenCountries": "atlas",
|
||||
"HiddenRegions": "atlas",
|
||||
"IdempotencyKeys": "common",
|
||||
"InviteTokens": "auth",
|
||||
"JourneyBooks": "journey",
|
||||
"JourneyContributors": "journey",
|
||||
"JourneyEntries": "journey",
|
||||
"JourneyEntryPhotos": "journey",
|
||||
"JourneyPhotos": "journey",
|
||||
"JourneyShareTokens": "journey",
|
||||
"JourneyTrips": "journey",
|
||||
"Journeys": "journey",
|
||||
"McpTokens": "tokens",
|
||||
"NotificationChannelPreferences": "notifications",
|
||||
"Notifications": "notifications",
|
||||
"OauthClients": "oauth",
|
||||
"OauthConsents": "oauth",
|
||||
"OauthTokens": "oauth",
|
||||
"PackingBags": "packing",
|
||||
"PackingCategoryAssignees": "packing",
|
||||
"PackingItemContributors": "packing",
|
||||
"PackingItems": "packing",
|
||||
"PackingTemplateCategories": "packing",
|
||||
"PackingTemplateItems": "packing",
|
||||
"PackingTemplates": "packing",
|
||||
"PasswordResetTokens": "auth",
|
||||
"PhotoProviderFields": "addons",
|
||||
"PhotoProviders": "addons",
|
||||
"PlaceDetailsCache": "maps",
|
||||
"PlaceRatings": "places",
|
||||
"PlaceRegions": "atlas",
|
||||
"PlaceShadowPicks": "place-shadow",
|
||||
"Places": "places",
|
||||
"PluginActions": "plugins",
|
||||
"PluginCapabilityAudit": "plugins",
|
||||
"PluginEgressHosts": "plugins",
|
||||
"PluginEntityMetadata": "plugins",
|
||||
"PluginErrorLog": "plugins",
|
||||
"PluginMetaMigrations": "plugins",
|
||||
"PluginOauthState": "plugins",
|
||||
"PluginOauthTokens": "plugins",
|
||||
"PluginScheduledTasks": "plugins",
|
||||
"PluginSettingsFields": "plugins",
|
||||
"PluginUserConfig": "plugins",
|
||||
"PluginUserErasureQueue": "plugins",
|
||||
"Plugins": "plugins",
|
||||
"PushSubscriptions": "notifications",
|
||||
"ReservationDayPositions": "reservations",
|
||||
"ReservationEndpoints": "reservations",
|
||||
"ReservationEndpointsEntity": "reservations",
|
||||
"ReservationTravelers": "reservations",
|
||||
"ReservationTravelersEntity": "reservations",
|
||||
"Reservations": "reservations",
|
||||
"RoadtripDayBoundaries": "roadtrip",
|
||||
"RoadtripDayTracks": "roadtrip",
|
||||
"RoadtripPreferencesEntity": "roadtrip",
|
||||
"RoadtripVias": "roadtrip",
|
||||
"RouteUsageDaily": "route-usage",
|
||||
"SchoolHolidayCountries": "school-holidays",
|
||||
"SchoolHolidayPeriods": "school-holidays",
|
||||
"SchoolHolidayRegions": "school-holidays",
|
||||
"Settings": "settings",
|
||||
"ShareTokens": "share",
|
||||
"Tags": "tags",
|
||||
"TodoCategoryAssignees": "todo",
|
||||
"TodoItems": "todo",
|
||||
"TourTypes": "tours",
|
||||
"TourWaypoints": "tours",
|
||||
"Tours": "tours",
|
||||
"TrekPhotoCacheMeta": "memories",
|
||||
"TrekPhotos": "photos",
|
||||
"TripAlbumLinks": "memories",
|
||||
"TripDocumentLinks": "doc-sync",
|
||||
"TripFiles": "files",
|
||||
"TripInviteTokens": "trip-invite",
|
||||
"TripMembers": "trip-membership",
|
||||
"TripPhotos": "photos",
|
||||
"Trips": "trips",
|
||||
"UserNoticeDismissals": "system-notices",
|
||||
"UserSessions": "sessions",
|
||||
"Users": "auth",
|
||||
"VacayCompanyHolidays": "vacay",
|
||||
"VacayEntries": "vacay",
|
||||
"VacayHolidayCalendars": "vacay",
|
||||
"VacayPlanMembers": "vacay",
|
||||
"VacayPlans": "vacay",
|
||||
"VacayShares": "vacay",
|
||||
"VacayUserColors": "vacay",
|
||||
"VacayUserSettingsEntity": "vacay",
|
||||
"VacayUserYears": "vacay",
|
||||
"VacayYears": "vacay",
|
||||
"VisitedCountries": "atlas",
|
||||
"VisitedRegions": "atlas",
|
||||
"WebauthnChallenges": "auth",
|
||||
"WebauthnCredentials": "auth"
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import { afterEach, describe, expect, it } from 'vitest';
|
||||
|
||||
const SCRIPT = path.join(__dirname, '../../../scripts/import-boundaries.mjs');
|
||||
|
||||
const EMPTY = { fileCycles: [], domainCycles: [], sharedImportsDomain: [], domainInternals: [], dbImportsNest: [] };
|
||||
const EMPTY = { fileCycles: [], domainCycles: [], sharedImportsDomain: [], domainInternals: [], dbImportsNest: [], foreignRepositories: [] };
|
||||
|
||||
interface ExecError {
|
||||
status: number | null;
|
||||
@@ -226,6 +226,32 @@ describe('import-boundaries.mjs', () => {
|
||||
expect(res.out).toContain('unresolved import x/a.ts -> ./missing');
|
||||
});
|
||||
|
||||
it('BOUND-012: fails on a repository another domain owns, passes the owner and a baselined entry', () => {
|
||||
const owners = JSON.stringify({ Trips: 'trips', Todos: 'todo' });
|
||||
const files = {
|
||||
'nest/trips/trips.service.ts': '@InjectRepository(Trips) class S {}\nexport { S };\n',
|
||||
'nest/todo/todo.service.ts': '@InjectRepository(Todos) class T {}\n@InjectRepository(Trips) class U {}\nexport { T, U };\n',
|
||||
};
|
||||
const withOwners = (baseline: unknown) => {
|
||||
const dir = serverRoot(files, baseline);
|
||||
writeFileSync(path.join(dir, 'scripts/repository-owners.json'), owners);
|
||||
return dir;
|
||||
};
|
||||
const bad = run(withOwners(EMPTY));
|
||||
expect(bad.status).toBe(1);
|
||||
expect(bad.out).toContain('FAIL foreignRepositories: todo -> Trips (owned by trips)');
|
||||
expect(bad.out).not.toContain('trips -> Trips');
|
||||
expect(bad.out).not.toContain('todo -> Todos');
|
||||
expect(run(withOwners({ ...EMPTY, foreignRepositories: ['todo -> Trips (owned by trips)'] })).status).toBe(0);
|
||||
});
|
||||
|
||||
it('BOUND-013: fails closed on an injected entity without an owner', () => {
|
||||
const dir = serverRoot({ 'nest/a/a.service.ts': '@InjectRepository(Mystery) class A {}\nexport { A };\n' });
|
||||
const res = run(dir);
|
||||
expect(res.status).toBe(1);
|
||||
expect(res.out).toContain('no owner for injected entity Mystery (nest/a/a.service.ts)');
|
||||
});
|
||||
|
||||
it('BOUND-010: resolves .js specifiers and index files', () => {
|
||||
const dir = serverRoot({
|
||||
'x/a.ts': "import { b } from './b.js';\nimport { c } from './c';\nexport const a = [b, c];\n",
|
||||
|
||||
Reference in New Issue
Block a user