@rhino-dev/rhino-nestjs 4.0.0 → 4.2.1
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/README.md +52 -1
- package/dist/blueprint/blueprint-runner.js +32 -1
- package/dist/blueprint/blueprint-runner.js.map +1 -1
- package/dist/blueprint/blueprint-sorter.d.ts +54 -0
- package/dist/blueprint/blueprint-sorter.js +107 -0
- package/dist/blueprint/blueprint-sorter.js.map +1 -0
- package/dist/controllers/auth.controller.d.ts +34 -5
- package/dist/controllers/auth.controller.js +132 -12
- package/dist/controllers/auth.controller.js.map +1 -1
- package/dist/controllers/invitation.controller.d.ts +41 -2
- package/dist/controllers/invitation.controller.js +97 -12
- package/dist/controllers/invitation.controller.js.map +1 -1
- package/dist/errors/rhino-exception.d.ts +12 -1
- package/dist/errors/rhino-exception.js +19 -1
- package/dist/errors/rhino-exception.js.map +1 -1
- package/dist/guards/group-membership.guard.d.ts +25 -0
- package/dist/guards/group-membership.guard.js +70 -0
- package/dist/guards/group-membership.guard.js.map +1 -0
- package/dist/guards/jwt-auth.guard.js +23 -4
- package/dist/guards/jwt-auth.guard.js.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +17 -2
- package/dist/index.js.map +1 -1
- package/dist/interfaces/rhino-config.interface.d.ts +71 -0
- package/dist/middleware/domain-route-resolver.d.ts +54 -0
- package/dist/middleware/domain-route-resolver.js +145 -0
- package/dist/middleware/domain-route-resolver.js.map +1 -0
- package/dist/middleware/route-group.middleware.d.ts +48 -4
- package/dist/middleware/route-group.middleware.js +274 -13
- package/dist/middleware/route-group.middleware.js.map +1 -1
- package/dist/rhino.config.d.ts +26 -1
- package/dist/rhino.config.js +56 -1
- package/dist/rhino.config.js.map +1 -1
- package/dist/rhino.module.d.ts +7 -0
- package/dist/rhino.module.js +23 -1
- package/dist/rhino.module.js.map +1 -1
- package/dist/services/auth-hooks.service.d.ts +27 -0
- package/dist/services/auth-hooks.service.js +85 -0
- package/dist/services/auth-hooks.service.js.map +1 -0
- package/dist/services/auth.service.d.ts +19 -0
- package/dist/services/auth.service.js +80 -5
- package/dist/services/auth.service.js.map +1 -1
- package/dist/services/invitation.service.d.ts +6 -0
- package/dist/services/invitation.service.js +45 -3
- package/dist/services/invitation.service.js.map +1 -1
- package/dist/services/membership.service.d.ts +56 -0
- package/dist/services/membership.service.js +113 -0
- package/dist/services/membership.service.js.map +1 -0
- package/dist/services/organization.service.js +8 -1
- package/dist/services/organization.service.js.map +1 -1
- package/dist/services/route-registration.service.d.ts +1 -0
- package/dist/services/route-registration.service.js +2 -0
- package/dist/services/route-registration.service.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/utils/domain-pattern.d.ts +36 -0
- package/dist/utils/domain-pattern.js +77 -0
- package/dist/utils/domain-pattern.js.map +1 -0
- package/dist/utils/permission-matcher.js +8 -0
- package/dist/utils/permission-matcher.js.map +1 -1
- package/dist/utils/route-group-validator.d.ts +14 -0
- package/dist/utils/route-group-validator.js +124 -0
- package/dist/utils/route-group-validator.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain pattern matching for route-group `domain` constraints.
|
|
3
|
+
*
|
|
4
|
+
* A route group's `domain` can be:
|
|
5
|
+
* - a literal host, e.g. `admin.example.com` (exact, case-insensitive match)
|
|
6
|
+
* - a parameterized host, e.g. `{organization}.example.com`, where the
|
|
7
|
+
* `{name}` segment is captured and exposed so that organization resolution
|
|
8
|
+
* can use it (subdomain multitenancy).
|
|
9
|
+
*
|
|
10
|
+
* Mirrors Laravel's `Route::domain(...)`, where a `{param}` in the domain is
|
|
11
|
+
* exposed as a route parameter that flows into ResolveOrganizationFromRoute.
|
|
12
|
+
*/
|
|
13
|
+
export interface CompiledDomain {
|
|
14
|
+
/** The raw pattern (as configured). */
|
|
15
|
+
pattern: string;
|
|
16
|
+
/** Whether the pattern contains at least one `{param}` placeholder. */
|
|
17
|
+
parameterized: boolean;
|
|
18
|
+
/** Ordered list of parameter names captured by the pattern. */
|
|
19
|
+
params: string[];
|
|
20
|
+
/** Anchored, case-insensitive regex used to test a host. */
|
|
21
|
+
regex: RegExp;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Compile a `domain` pattern into a matcher. Returns `null` for an empty /
|
|
25
|
+
* missing pattern (meaning "match any host").
|
|
26
|
+
*/
|
|
27
|
+
export declare function compileDomain(pattern: string | undefined | null): CompiledDomain | null;
|
|
28
|
+
export interface DomainMatch {
|
|
29
|
+
/** Captured `{param}` values keyed by name (empty for literal domains). */
|
|
30
|
+
params: Record<string, string>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Test a host against a compiled domain pattern. Returns the captured params
|
|
34
|
+
* on success, or `null` when the host does not match.
|
|
35
|
+
*/
|
|
36
|
+
export declare function matchDomain(compiled: CompiledDomain, host: string | undefined | null): DomainMatch | null;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Domain pattern matching for route-group `domain` constraints.
|
|
4
|
+
*
|
|
5
|
+
* A route group's `domain` can be:
|
|
6
|
+
* - a literal host, e.g. `admin.example.com` (exact, case-insensitive match)
|
|
7
|
+
* - a parameterized host, e.g. `{organization}.example.com`, where the
|
|
8
|
+
* `{name}` segment is captured and exposed so that organization resolution
|
|
9
|
+
* can use it (subdomain multitenancy).
|
|
10
|
+
*
|
|
11
|
+
* Mirrors Laravel's `Route::domain(...)`, where a `{param}` in the domain is
|
|
12
|
+
* exposed as a route parameter that flows into ResolveOrganizationFromRoute.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.compileDomain = compileDomain;
|
|
16
|
+
exports.matchDomain = matchDomain;
|
|
17
|
+
const PARAM_RE = /\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
18
|
+
function escapeRegex(s) {
|
|
19
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Compile a `domain` pattern into a matcher. Returns `null` for an empty /
|
|
23
|
+
* missing pattern (meaning "match any host").
|
|
24
|
+
*/
|
|
25
|
+
function compileDomain(pattern) {
|
|
26
|
+
if (pattern == null)
|
|
27
|
+
return null;
|
|
28
|
+
const trimmed = String(pattern).trim();
|
|
29
|
+
if (trimmed === '')
|
|
30
|
+
return null;
|
|
31
|
+
const params = [];
|
|
32
|
+
let lastIndex = 0;
|
|
33
|
+
let source = '';
|
|
34
|
+
PARAM_RE.lastIndex = 0;
|
|
35
|
+
let m;
|
|
36
|
+
while ((m = PARAM_RE.exec(trimmed)) !== null) {
|
|
37
|
+
// literal text before the placeholder
|
|
38
|
+
source += escapeRegex(trimmed.slice(lastIndex, m.index));
|
|
39
|
+
const name = m[1];
|
|
40
|
+
params.push(name);
|
|
41
|
+
// a host label cannot contain a dot — capture a single label segment
|
|
42
|
+
source += `(?<${name}>[^.]+)`;
|
|
43
|
+
lastIndex = m.index + m[0].length;
|
|
44
|
+
}
|
|
45
|
+
source += escapeRegex(trimmed.slice(lastIndex));
|
|
46
|
+
return {
|
|
47
|
+
pattern: trimmed,
|
|
48
|
+
parameterized: params.length > 0,
|
|
49
|
+
params,
|
|
50
|
+
regex: new RegExp(`^${source}$`, 'i'),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Test a host against a compiled domain pattern. Returns the captured params
|
|
55
|
+
* on success, or `null` when the host does not match.
|
|
56
|
+
*/
|
|
57
|
+
function matchDomain(compiled, host) {
|
|
58
|
+
if (host == null)
|
|
59
|
+
return null;
|
|
60
|
+
// Strip an optional port (Express `req.hostname` excludes it, but be safe).
|
|
61
|
+
const cleaned = String(host).split(':')[0];
|
|
62
|
+
if (cleaned === '')
|
|
63
|
+
return null;
|
|
64
|
+
const result = compiled.regex.exec(cleaned);
|
|
65
|
+
if (!result)
|
|
66
|
+
return null;
|
|
67
|
+
const params = {};
|
|
68
|
+
if (result.groups) {
|
|
69
|
+
for (const name of compiled.params) {
|
|
70
|
+
const value = result.groups[name];
|
|
71
|
+
if (value !== undefined)
|
|
72
|
+
params[name] = value;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return { params };
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=domain-pattern.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"domain-pattern.js","sourceRoot":"","sources":["../../src/utils/domain-pattern.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;GAWG;;AAuBH,sCA2BC;AAWD,kCAeC;AA/DD,MAAM,QAAQ,GAAG,+BAA+B,CAAC;AAEjD,SAAS,WAAW,CAAC,CAAS;IAC5B,OAAO,CAAC,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;AAClD,CAAC;AAED;;;GAGG;AACH,SAAgB,aAAa,CAAC,OAAkC;IAC9D,IAAI,OAAO,IAAI,IAAI;QAAE,OAAO,IAAI,CAAC;IACjC,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;IACvC,IAAI,OAAO,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IAEhC,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,SAAS,GAAG,CAAC,CAAC;IAClB,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,QAAQ,CAAC,SAAS,GAAG,CAAC,CAAC;IACvB,IAAI,CAAyB,CAAC;IAC9B,OAAO,CAAC,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QAC7C,sCAAsC;QACtC,MAAM,IAAI,WAAW,CAAC,OAAO,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;QACzD,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QAClB,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAClB,qEAAqE;QACrE,MAAM,IAAI,MAAM,IAAI,SAAS,CAAC;QAC9B,SAAS,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IACpC,CAAC;IACD,MAAM,IAAI,WAAW,CAAC,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC;IAEhD,OAAO;QACL,OAAO,EAAE,OAAO;QAChB,aAAa,EAAE,MAAM,CAAC,MAAM,GAAG,CAAC;QAChC,MAAM;QACN,KAAK,EAAE,IAAI,MAAM,CAAC,IAAI,MAAM,GAAG,EAAE,GAAG,CAAC;KACtC,CAAC;AACJ,CAAC;AAOD;;;GAGG;AACH,SAAgB,WAAW,CAAC,QAAwB,EAAE,IAA+B;IACnF,IAAI,IAAI,IAAI,IAAI;QAAE,OAAO,IAAI,CAAC;IAC9B,4EAA4E;IAC5E,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3C,IAAI,OAAO,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IAChC,MAAM,MAAM,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC5C,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IACzB,MAAM,MAAM,GAA2B,EAAE,CAAC;IAC1C,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;QAClB,KAAK,MAAM,IAAI,IAAI,QAAQ,CAAC,MAAM,EAAE,CAAC;YACnC,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YAClC,IAAI,KAAK,KAAK,SAAS;gBAAE,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;QAChD,CAAC;IACH,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,CAAC;AACpB,CAAC"}
|
|
@@ -91,6 +91,14 @@ function resolveUserRoleSlug(user, organizationId) {
|
|
|
91
91
|
function resolveUserPermissions(user, organizationId) {
|
|
92
92
|
if (!user)
|
|
93
93
|
return [];
|
|
94
|
+
// Group-membership enforcement (design §6): when the membership layer has
|
|
95
|
+
// resolved the matching row(s), the permission source switches to that row
|
|
96
|
+
// only. The guard attaches the resolved list as `__membershipPermissions`.
|
|
97
|
+
// Absent (enforcement off) → fall through to the legacy heuristic, so
|
|
98
|
+
// existing behavior is byte-for-byte unchanged.
|
|
99
|
+
if (Array.isArray(user.__membershipPermissions)) {
|
|
100
|
+
return coercePermissions(user.__membershipPermissions);
|
|
101
|
+
}
|
|
94
102
|
if (organizationId != null) {
|
|
95
103
|
const userRoles = user.userRoles ?? user.user_roles ?? [];
|
|
96
104
|
const all = [];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"permission-matcher.js","sourceRoot":"","sources":["../../src/utils/permission-matcher.ts"],"names":[],"mappings":";;AAeA,8CAiBC;AAaD,8CAYC;AAMD,kDAUC;AAQD,
|
|
1
|
+
{"version":3,"file":"permission-matcher.js","sourceRoot":"","sources":["../../src/utils/permission-matcher.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { RhinoConfig } from '../interfaces/rhino-config.interface';
|
|
2
|
+
/**
|
|
3
|
+
* Thrown at config-normalization (boot) time when two route groups would
|
|
4
|
+
* resolve to the same routes and silently shadow one another.
|
|
5
|
+
*
|
|
6
|
+
* This happens when two groups share the same effective prefix, have
|
|
7
|
+
* intersecting host-sets (no `domain` = matches every host; identical domain
|
|
8
|
+
* pattern), and register overlapping models. Failing fast prevents a dangerous
|
|
9
|
+
* misconfiguration (e.g. a `skipAuth` group shadowing an authenticated one).
|
|
10
|
+
*/
|
|
11
|
+
export declare class RouteGroupConflictError extends Error {
|
|
12
|
+
constructor(message: string);
|
|
13
|
+
}
|
|
14
|
+
export declare function validateRouteGroups(config: RhinoConfig): void;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RouteGroupConflictError = void 0;
|
|
4
|
+
exports.validateRouteGroups = validateRouteGroups;
|
|
5
|
+
/**
|
|
6
|
+
* Thrown at config-normalization (boot) time when two route groups would
|
|
7
|
+
* resolve to the same routes and silently shadow one another.
|
|
8
|
+
*
|
|
9
|
+
* This happens when two groups share the same effective prefix, have
|
|
10
|
+
* intersecting host-sets (no `domain` = matches every host; identical domain
|
|
11
|
+
* pattern), and register overlapping models. Failing fast prevents a dangerous
|
|
12
|
+
* misconfiguration (e.g. a `skipAuth` group shadowing an authenticated one).
|
|
13
|
+
*/
|
|
14
|
+
class RouteGroupConflictError extends Error {
|
|
15
|
+
constructor(message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = 'RouteGroupConflictError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
exports.RouteGroupConflictError = RouteGroupConflictError;
|
|
21
|
+
function normalizePrefix(group) {
|
|
22
|
+
return String(group.prefix ?? '');
|
|
23
|
+
}
|
|
24
|
+
/** An undefined/blank domain means "any host". */
|
|
25
|
+
function normalizeDomain(group) {
|
|
26
|
+
const domain = group.domain;
|
|
27
|
+
if (domain == null || String(domain).trim() === '')
|
|
28
|
+
return null;
|
|
29
|
+
return String(domain);
|
|
30
|
+
}
|
|
31
|
+
function hostSetsIntersect(a, b) {
|
|
32
|
+
const da = normalizeDomain(a);
|
|
33
|
+
const db = normalizeDomain(b);
|
|
34
|
+
// A wildcard host (no domain) intersects with any other host-set.
|
|
35
|
+
if (da === null || db === null)
|
|
36
|
+
return true;
|
|
37
|
+
// Two explicit domain patterns intersect only when identical.
|
|
38
|
+
return da === db;
|
|
39
|
+
}
|
|
40
|
+
function resolveModels(group, allModels) {
|
|
41
|
+
if (group.models === '*')
|
|
42
|
+
return Object.keys(allModels ?? {});
|
|
43
|
+
return group.models ?? [];
|
|
44
|
+
}
|
|
45
|
+
function overlappingModels(a, b, allModels) {
|
|
46
|
+
const ma = new Set(resolveModels(a, allModels));
|
|
47
|
+
return resolveModels(b, allModels).filter((slug) => ma.has(slug));
|
|
48
|
+
}
|
|
49
|
+
function buildMessage(aName, bName, a, b, shared) {
|
|
50
|
+
const prefix = normalizePrefix(a);
|
|
51
|
+
const prefixLabel = prefix === '' ? '(root)' : `'${prefix}'`;
|
|
52
|
+
const da = normalizeDomain(a);
|
|
53
|
+
const db = normalizeDomain(b);
|
|
54
|
+
const domainLabel = da === null && db === null
|
|
55
|
+
? 'no domain'
|
|
56
|
+
: `domains [${da ?? 'any'}, ${db ?? 'any'}]`;
|
|
57
|
+
return (`Route groups '${aName}' and '${bName}' conflict: they share prefix ` +
|
|
58
|
+
`${prefixLabel} with ${domainLabel} and overlapping models (${shared.join(', ')}), ` +
|
|
59
|
+
`so one would silently shadow the other. Give them distinct prefixes, or ` +
|
|
60
|
+
`distinguish them with different 'domain' values, or make their 'models' disjoint.`);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Validate the configured route groups and throw RouteGroupConflictError when
|
|
64
|
+
* two groups would silently shadow each other.
|
|
65
|
+
*
|
|
66
|
+
* A route group's routing identity is the pair (host-set, prefix), per model.
|
|
67
|
+
* Two groups conflict when ALL of the following hold:
|
|
68
|
+
*
|
|
69
|
+
* 1. Their host-sets intersect (a group with no `domain` is a wildcard host).
|
|
70
|
+
* 2. They share the same effective prefix (undefined/'' is the same root).
|
|
71
|
+
* 3. Their model sets overlap ('*' expands to every registered model).
|
|
72
|
+
*
|
|
73
|
+
* With a distinguishing domain, the prefix is optional (the host disambiguates);
|
|
74
|
+
* without a domain, the prefix is the only disambiguator, so two or more
|
|
75
|
+
* overlapping groups must use distinct prefixes.
|
|
76
|
+
*
|
|
77
|
+
* Note: conservative static check — exotic cross-pattern overlaps (a literal
|
|
78
|
+
* host that also satisfies another group's `{param}.example.com`) are not
|
|
79
|
+
* statically detected.
|
|
80
|
+
*/
|
|
81
|
+
/**
|
|
82
|
+
* Whether a group registers the legacy/global (empty-prefix, no-domain) auth
|
|
83
|
+
* route set. Per design §11.1, such an auth-enabled group IS the default auth
|
|
84
|
+
* path. Two or more of them are genuinely indistinguishable — their auth routes
|
|
85
|
+
* (and hooks/membership) would collide with no way to tell them apart — so the
|
|
86
|
+
* validator must reject the config at boot.
|
|
87
|
+
*/
|
|
88
|
+
function isIndistinguishableAuthGroup(group) {
|
|
89
|
+
return (group.auth === true &&
|
|
90
|
+
normalizePrefix(group) === '' &&
|
|
91
|
+
normalizeDomain(group) === null);
|
|
92
|
+
}
|
|
93
|
+
function validateRouteGroups(config) {
|
|
94
|
+
const groups = config.routeGroups ?? {};
|
|
95
|
+
const models = config.models ?? {};
|
|
96
|
+
const names = Object.keys(groups);
|
|
97
|
+
// FIX 11.1: two+ auth-enabled groups with empty prefix AND no domain are
|
|
98
|
+
// genuinely indistinguishable — they'd register the same legacy `/auth/*`
|
|
99
|
+
// routes. The `public` group is never auth-enabled, so it can't collide here.
|
|
100
|
+
const indistinguishableAuth = names.filter((n) => n !== 'public' && isIndistinguishableAuthGroup(groups[n]));
|
|
101
|
+
if (indistinguishableAuth.length >= 2) {
|
|
102
|
+
const [a, b] = indistinguishableAuth;
|
|
103
|
+
throw new RouteGroupConflictError(`Auth-enabled route groups '${a}' and '${b}' are indistinguishable: ` +
|
|
104
|
+
`both have an empty prefix and no domain, so they would register the ` +
|
|
105
|
+
`same legacy auth routes and their hooks/membership could not be told ` +
|
|
106
|
+
`apart. Give one a distinct 'prefix' or a 'domain', or disable 'auth' ` +
|
|
107
|
+
`on all but one.`);
|
|
108
|
+
}
|
|
109
|
+
for (let i = 0; i < names.length; i++) {
|
|
110
|
+
for (let j = i + 1; j < names.length; j++) {
|
|
111
|
+
const a = groups[names[i]];
|
|
112
|
+
const b = groups[names[j]];
|
|
113
|
+
if (!hostSetsIntersect(a, b))
|
|
114
|
+
continue;
|
|
115
|
+
if (normalizePrefix(a) !== normalizePrefix(b))
|
|
116
|
+
continue;
|
|
117
|
+
const shared = overlappingModels(a, b, models);
|
|
118
|
+
if (shared.length === 0)
|
|
119
|
+
continue;
|
|
120
|
+
throw new RouteGroupConflictError(buildMessage(names[i], names[j], a, b, shared));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=route-group-validator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route-group-validator.js","sourceRoot":"","sources":["../../src/utils/route-group-validator.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED