@open-rlb/nestjs-amqp 2.0.4 → 2.0.5
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 +4 -2
- package/amqp-lib/config/rabbitmq.config.d.ts +6 -0
- package/modules/acl/const.d.ts +0 -1
- package/modules/acl/const.js +0 -1
- package/modules/acl/const.js.map +1 -1
- package/modules/acl/models.d.ts +5 -7
- package/modules/acl/repository/acl-action.repository.d.ts +1 -5
- package/modules/acl/repository/acl-action.repository.js.map +1 -1
- package/modules/acl/repository/acl-role.repository.d.ts +1 -5
- package/modules/acl/repository/acl-role.repository.js.map +1 -1
- package/modules/acl/services/acl-management.service.d.ts +2 -2
- package/modules/acl/services/acl-management.service.js +17 -20
- package/modules/acl/services/acl-management.service.js.map +1 -1
- package/modules/acl/services/acl.service.d.ts +1 -2
- package/modules/acl/services/acl.service.js +5 -21
- package/modules/acl/services/acl.service.js.map +1 -1
- package/modules/broker/broker.module.d.ts +2 -4
- package/modules/broker/broker.module.js +23 -5
- package/modules/broker/broker.module.js.map +1 -1
- package/modules/broker/config/route-discovery.config.d.ts +2 -0
- package/modules/broker/const.d.ts +1 -0
- package/modules/broker/const.js +2 -1
- package/modules/broker/const.js.map +1 -1
- package/modules/broker/services/broker.service.js +1 -1
- package/modules/broker/services/broker.service.js.map +1 -1
- package/modules/broker/services/route-discovery-publisher.service.js +7 -5
- package/modules/broker/services/route-discovery-publisher.service.js.map +1 -1
- package/modules/gateway-admin/config/gateway-admin.config.d.ts +4 -0
- package/modules/gateway-admin/const.d.ts +1 -1
- package/modules/gateway-admin/const.js +1 -1
- package/modules/gateway-admin/const.js.map +1 -1
- package/modules/gateway-admin/gateway-admin.module.d.ts +7 -1
- package/modules/gateway-admin/gateway-admin.module.js +13 -0
- package/modules/gateway-admin/gateway-admin.module.js.map +1 -1
- package/modules/gateway-admin/repository/auth-provider.repository.d.ts +3 -4
- package/modules/gateway-admin/repository/auth-provider.repository.js.map +1 -1
- package/modules/gateway-admin/services/gateway-auth.service.d.ts +3 -4
- package/modules/gateway-admin/services/gateway-auth.service.js +15 -23
- package/modules/gateway-admin/services/gateway-auth.service.js.map +1 -1
- package/modules/gateway-admin/services/gateway-metrics.service.d.ts +3 -0
- package/modules/gateway-admin/services/gateway-metrics.service.js +9 -0
- package/modules/gateway-admin/services/gateway-metrics.service.js.map +1 -1
- package/modules/gateway-admin/services/route-sync.service.d.ts +3 -1
- package/modules/gateway-admin/services/route-sync.service.js +14 -8
- package/modules/gateway-admin/services/route-sync.service.js.map +1 -1
- package/modules/proxy/services/http-handler.service.d.ts +3 -0
- package/modules/proxy/services/http-handler.service.js +27 -3
- package/modules/proxy/services/http-handler.service.js.map +1 -1
- package/package.json +5 -1
- package/schematics/nest-add/files/acl/src/cache/in-memory-acl-store.ts +54 -0
- package/schematics/nest-add/files/acl/src/modules/database/repository/acl.repository.ts +74 -0
- package/schematics/nest-add/files/db-core/src/modules/database/repository/in-memory-collection.ts +122 -0
- package/schematics/nest-add/files/gateway-admin/src/modules/database/repository/gateway.repository.ts +151 -0
- package/schematics/nest-add/files/gateway-admin/src/modules/database/repository/route-sync.repository.ts +15 -0
- package/schematics/nest-add/files/skills/rlb-amqp/SKILL.md +30 -5
- package/schematics/nest-add/files/skills/rlb-amqp/references/config-schema.md +182 -93
- package/schematics/nest-add/files/skills/rlb-amqp/references/gotchas.md +120 -79
- package/schematics/nest-add/files/skills/rlb-amqp-acl/SKILL.md +185 -0
- package/schematics/nest-add/files/skills/rlb-amqp-add-action/SKILL.md +49 -2
- package/schematics/nest-add/files/skills/rlb-amqp-add-route/SKILL.md +82 -19
- package/schematics/nest-add/files/skills/rlb-amqp-add-ws-event/SKILL.md +40 -18
- package/schematics/nest-add/files/skills/rlb-amqp-gateway-admin/SKILL.md +233 -0
- package/schematics/nest-add/files/skills/rlb-amqp-scaffold/SKILL.md +172 -42
- package/schematics/nest-add/index.js +612 -142
- package/schematics/nest-add/index.js.map +1 -1
- package/schematics/nest-add/index.ts +673 -241
- package/schematics/nest-add/init.schema.d.ts +10 -1
- package/schematics/nest-add/init.schema.ts +29 -3
- package/schematics/nest-add/schema.json +37 -8
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { AclCacheStore } from '@open-rlb/nestjs-amqp';
|
|
3
|
+
|
|
4
|
+
interface Entry { value: string; exp: number; }
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* In-memory L2 ACL cache for the example app. Replaces the private Redis-backed store so
|
|
8
|
+
* the example builds without external/private packages. It implements the same
|
|
9
|
+
* RLB_ACL_CACHE_STORE contract (string '1'/'0' decisions with a TTL) using a plain Map.
|
|
10
|
+
*
|
|
11
|
+
* NOT for production / multi-instance: the map is per-process and not shared, so cache
|
|
12
|
+
* invalidations on one instance won't reach the others. For a real deployment, plug a
|
|
13
|
+
* shared store (Redis, etc.) under RLB_ACL_CACHE_STORE instead.
|
|
14
|
+
*/
|
|
15
|
+
@Injectable()
|
|
16
|
+
export class InMemoryAclStore implements AclCacheStore {
|
|
17
|
+
private readonly store = new Map<string, Entry>();
|
|
18
|
+
|
|
19
|
+
async get(key: string): Promise<string | null | undefined> {
|
|
20
|
+
const entry = this.store.get(key);
|
|
21
|
+
if (!entry) return null;
|
|
22
|
+
if (entry.exp <= Date.now()) {
|
|
23
|
+
this.store.delete(key);
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return entry.value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async set(key: string, value: string, ttlSeconds: number): Promise<void> {
|
|
30
|
+
this.store.set(key, { value, exp: Date.now() + ttlSeconds * 1000 });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async del(keys: string[]): Promise<void> {
|
|
34
|
+
for (const key of keys) this.store.delete(key);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async keys(pattern: string): Promise<string[]> {
|
|
38
|
+
// AclCacheService only uses simple globs like `acl/<userId>/*` (or `acl/*`).
|
|
39
|
+
const regExp = this.globToRegExp(pattern);
|
|
40
|
+
const out: string[] = [];
|
|
41
|
+
for (const key of this.store.keys()) {
|
|
42
|
+
if (regExp.test(key)) out.push(key);
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private globToRegExp(pattern: string): RegExp {
|
|
48
|
+
const escaped = pattern
|
|
49
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
50
|
+
.replace(/\*/g, '.*')
|
|
51
|
+
.replace(/\?/g, '.');
|
|
52
|
+
return new RegExp(`^${escaped}$`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import {
|
|
3
|
+
AclAction,
|
|
4
|
+
AclActionRepository,
|
|
5
|
+
AclGrant,
|
|
6
|
+
AclGrantRepository,
|
|
7
|
+
AclRole,
|
|
8
|
+
AclRoleRepository,
|
|
9
|
+
PaginationModel,
|
|
10
|
+
} from '@open-rlb/nestjs-amqp';
|
|
11
|
+
import { InMemoryCollection } from './in-memory-collection';
|
|
12
|
+
|
|
13
|
+
const page1 = (p?: number) => Number(p) || 1;
|
|
14
|
+
const lim10 = (l?: number) => Number(l) || 10;
|
|
15
|
+
|
|
16
|
+
@Injectable()
|
|
17
|
+
export class InMemoryAclActionRepository extends AclActionRepository {
|
|
18
|
+
// Storage keeps an internal _id (the collection's key); the public AclAction has none.
|
|
19
|
+
private readonly col = new InMemoryCollection<AclAction & { _id?: string }>();
|
|
20
|
+
|
|
21
|
+
async insert(model: AclAction): Promise<AclAction> { return this.col.insert(model); }
|
|
22
|
+
async insertMany(models: AclAction[]): Promise<AclAction[]> { return this.col.insertMany(models); }
|
|
23
|
+
async findByName(name: string): Promise<AclAction> { return this.col.findOne({ name })!; }
|
|
24
|
+
async findOne(filter: Record<string, any>): Promise<AclAction> { return this.col.findOne(filter)!; }
|
|
25
|
+
async upsertOne(filter: Record<string, any>, model: Partial<AclAction>): Promise<AclAction> { return this.col.upsertOne(filter, model); }
|
|
26
|
+
async updateOne(filter: Record<string, any>, model: Partial<AclAction>): Promise<AclAction> { return this.col.updateOne(filter, model)!; }
|
|
27
|
+
async mergeOne(filter: Record<string, any>, model: Partial<AclAction>): Promise<AclAction> { return this.col.updateOne(filter, model)!; }
|
|
28
|
+
async removeOne(filter: Record<string, any>): Promise<AclAction> { return this.col.removeOne(filter)!; }
|
|
29
|
+
async removeMany(filter: Record<string, any>): Promise<number> { return this.col.removeMany(filter); }
|
|
30
|
+
async filter(filter: Record<string, any>): Promise<AclAction[]> { return this.col.filter(filter); }
|
|
31
|
+
async filterPaginated(filter: Record<string, any>, page?: number, limit?: number): Promise<PaginationModel<AclAction>> { return this.col.paginate(filter, page1(page), lim10(limit)); }
|
|
32
|
+
async retrieveAll(): Promise<AclAction[]> { return this.col.all(); }
|
|
33
|
+
async retrieveAllPaginated(page: number, limit: number): Promise<PaginationModel<AclAction>> { return this.col.paginate({}, page1(page), lim10(limit)); }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@Injectable()
|
|
37
|
+
export class InMemoryAclRoleRepository extends AclRoleRepository {
|
|
38
|
+
// Storage keeps an internal _id (the collection's key); the public AclRole has none.
|
|
39
|
+
private readonly col = new InMemoryCollection<AclRole & { _id?: string }>();
|
|
40
|
+
|
|
41
|
+
async insert(model: AclRole): Promise<AclRole> { return this.col.insert(model); }
|
|
42
|
+
async insertMany(models: AclRole[]): Promise<AclRole[]> { return this.col.insertMany(models); }
|
|
43
|
+
async findByName(name: string): Promise<AclRole> { return this.col.findOne({ name })!; }
|
|
44
|
+
async findOne(filter: Record<string, any>): Promise<AclRole> { return this.col.findOne(filter)!; }
|
|
45
|
+
async upsertOne(filter: Record<string, any>, model: Partial<AclRole>): Promise<AclRole> { return this.col.upsertOne(filter, model); }
|
|
46
|
+
async updateOne(filter: Record<string, any>, model: Partial<AclRole>): Promise<AclRole> { return this.col.updateOne(filter, model)!; }
|
|
47
|
+
async removeOne(filter: Record<string, any>): Promise<AclRole> { return this.col.removeOne(filter)!; }
|
|
48
|
+
async filter(filter: Record<string, any>): Promise<AclRole[]> { return this.col.filter(filter); }
|
|
49
|
+
async filterPaginated(filter: Record<string, any>, page?: number, limit?: number): Promise<PaginationModel<AclRole>> { return this.col.paginate(filter, page1(page), lim10(limit)); }
|
|
50
|
+
async list(): Promise<AclRole[]> { return this.col.all(); }
|
|
51
|
+
async listPaginated(page: number, limit: number): Promise<PaginationModel<AclRole>> { return this.col.paginate({}, page1(page), lim10(limit)); }
|
|
52
|
+
|
|
53
|
+
async getActionsByNames(names: string[]): Promise<string[]> {
|
|
54
|
+
if (!names?.length) return [];
|
|
55
|
+
const roles = this.col.filter({ name: { $in: names } });
|
|
56
|
+
return [...new Set(roles.flatMap((r) => r.actions || []))];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@Injectable()
|
|
61
|
+
export class InMemoryAclGrantRepository extends AclGrantRepository {
|
|
62
|
+
private readonly col = new InMemoryCollection<AclGrant>();
|
|
63
|
+
|
|
64
|
+
async insert(model: AclGrant): Promise<AclGrant> { return this.col.insert(model); }
|
|
65
|
+
async findById(id: string): Promise<AclGrant> { return this.col.findById(id)!; }
|
|
66
|
+
async findOne(filter: Record<string, any>): Promise<AclGrant> { return this.col.findOne(filter)!; }
|
|
67
|
+
async updateById(id: string, model: Partial<AclGrant>): Promise<AclGrant> { return this.col.updateById(id, model)!; }
|
|
68
|
+
async updateOne(filter: Record<string, any>, model: Partial<AclGrant>): Promise<AclGrant> { return this.col.updateOne(filter, model)!; }
|
|
69
|
+
async mergeById(id: string, model: Partial<AclGrant>): Promise<AclGrant> { return this.col.updateById(id, model)!; }
|
|
70
|
+
async removeById(id: string): Promise<AclGrant> { return this.col.removeById(id)!; }
|
|
71
|
+
async removeOne(filter: Record<string, any>): Promise<AclGrant> { return this.col.removeOne(filter)!; }
|
|
72
|
+
async filter(filter: Record<string, any>): Promise<AclGrant[]> { return this.col.filter(filter); }
|
|
73
|
+
async filterPaginated(filter: Record<string, any>, page?: number, limit?: number): Promise<PaginationModel<AclGrant>> { return this.col.paginate(filter, page1(page), lim10(limit)); }
|
|
74
|
+
}
|
package/schematics/nest-add/files/db-core/src/modules/database/repository/in-memory-collection.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { PaginationModel } from '@open-rlb/nestjs-amqp';
|
|
2
|
+
|
|
3
|
+
let __seq = 0;
|
|
4
|
+
/** Monotonic, ObjectId-looking id (24 hex chars) — no randomness needed. */
|
|
5
|
+
function nextId(): string {
|
|
6
|
+
return (++__seq).toString(16).padStart(24, '0');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** True when `item` matches a simple filter: field equality and `{ $in: [...] }`. */
|
|
10
|
+
function matches(item: any, filter: Record<string, any>): boolean {
|
|
11
|
+
for (const key of Object.keys(filter || {})) {
|
|
12
|
+
const cond = filter[key];
|
|
13
|
+
if (cond === undefined) continue;
|
|
14
|
+
if (cond && typeof cond === 'object' && Array.isArray(cond.$in)) {
|
|
15
|
+
if (!cond.$in.includes(item[key])) return false;
|
|
16
|
+
} else if (item[key] !== cond) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Tiny in-memory collection that mimics the slice of Mongo behavior the repositories
|
|
25
|
+
* need (CRUD by id/filter, `$in`, pagination). Returns shallow copies so callers can't
|
|
26
|
+
* mutate stored documents. Used by the example's RAM repositories instead of Mongoose.
|
|
27
|
+
*/
|
|
28
|
+
export class InMemoryCollection<T extends { _id?: string }> {
|
|
29
|
+
private readonly items = new Map<string, T>();
|
|
30
|
+
|
|
31
|
+
insert(model: T): T {
|
|
32
|
+
const _id = model._id ?? nextId();
|
|
33
|
+
const stored = { ...model, _id } as T;
|
|
34
|
+
this.items.set(_id, stored);
|
|
35
|
+
return { ...stored };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
insertMany(models: T[]): T[] {
|
|
39
|
+
return (models || []).map((m) => this.insert(m));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private findId(filter: Record<string, any>): string | undefined {
|
|
43
|
+
for (const [id, v] of this.items) if (matches(v, filter)) return id;
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
upsertById(id: string, patch: Partial<T>): T {
|
|
48
|
+
return this.items.has(id) ? this.updateById(id, patch)! : this.insert({ ...(patch as object), _id: id } as T);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
upsertOne(filter: Record<string, any>, patch: Partial<T>): T {
|
|
52
|
+
const id = this.findId(filter);
|
|
53
|
+
return id ? this.updateById(id, patch)! : this.insert(patch as T);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
removeMany(filter: Record<string, any>): number {
|
|
57
|
+
let removed = 0;
|
|
58
|
+
for (const [id, v] of [...this.items]) if (matches(v, filter)) { this.items.delete(id); removed++; }
|
|
59
|
+
return removed;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
findById(id: string): T | undefined {
|
|
63
|
+
const v = this.items.get(id);
|
|
64
|
+
return v ? { ...v } : undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
findOne(filter: Record<string, any>): T | undefined {
|
|
68
|
+
for (const v of this.items.values()) if (matches(v, filter)) return { ...v };
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
filter(filter: Record<string, any>): T[] {
|
|
73
|
+
return [...this.items.values()].filter((v) => matches(v, filter)).map((v) => ({ ...v }));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
updateById(id: string, patch: Partial<T>): T | undefined {
|
|
77
|
+
const v = this.items.get(id);
|
|
78
|
+
if (!v) return undefined;
|
|
79
|
+
const next = { ...v, ...patch, _id: id } as T;
|
|
80
|
+
this.items.set(id, next);
|
|
81
|
+
return { ...next };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
updateOne(filter: Record<string, any>, patch: Partial<T>): T | undefined {
|
|
85
|
+
for (const [id, v] of this.items) {
|
|
86
|
+
if (matches(v, filter)) {
|
|
87
|
+
const next = { ...v, ...patch, _id: id } as T;
|
|
88
|
+
this.items.set(id, next);
|
|
89
|
+
return { ...next };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
removeById(id: string): T | undefined {
|
|
96
|
+
const v = this.items.get(id);
|
|
97
|
+
if (!v) return undefined;
|
|
98
|
+
this.items.delete(id);
|
|
99
|
+
return { ...v };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
removeOne(filter: Record<string, any>): T | undefined {
|
|
103
|
+
for (const [id, v] of this.items) {
|
|
104
|
+
if (matches(v, filter)) {
|
|
105
|
+
this.items.delete(id);
|
|
106
|
+
return { ...v };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
all(): T[] {
|
|
113
|
+
return [...this.items.values()].map((v) => ({ ...v }));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
paginate(filter: Record<string, any>, page = 1, limit = 10): PaginationModel<T> {
|
|
117
|
+
const data = this.filter(filter);
|
|
118
|
+
const p = page < 1 ? 1 : page;
|
|
119
|
+
const start = (p - 1) * limit;
|
|
120
|
+
return { page: p, limit, total: data.length, data: data.slice(start, start + limit) };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import {
|
|
3
|
+
AuthProviderRepository,
|
|
4
|
+
HandlerAuthConfig,
|
|
5
|
+
HttpMetric,
|
|
6
|
+
HttpMetricPoint,
|
|
7
|
+
HttpMetricRepository,
|
|
8
|
+
HttpPathRepository,
|
|
9
|
+
MetricQuery,
|
|
10
|
+
MetricSeriesBucket,
|
|
11
|
+
MetricSeriesQuery,
|
|
12
|
+
PaginationModel,
|
|
13
|
+
PathDefinition,
|
|
14
|
+
StoredAuthProvider,
|
|
15
|
+
StoredHttpPath,
|
|
16
|
+
TrackCallInput,
|
|
17
|
+
} from '@open-rlb/nestjs-amqp';
|
|
18
|
+
import { InMemoryCollection } from './in-memory-collection';
|
|
19
|
+
|
|
20
|
+
/** True when a metric point matches the query's method/route/name + [from,to] time window. */
|
|
21
|
+
function matchPoint(p: HttpMetricPoint, q: MetricQuery): boolean {
|
|
22
|
+
if (q.method && p.method !== q.method) return false;
|
|
23
|
+
if (q.route && p.route !== q.route) return false;
|
|
24
|
+
if (q.name && p.name !== q.name) return false;
|
|
25
|
+
if (q.from != null && p.ts < q.from) return false;
|
|
26
|
+
if (q.to != null && p.ts > q.to) return false;
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Drops persistence-only fields (_id, enabled) from a stored doc. */
|
|
31
|
+
function toPlain<T extends { _id?: string; enabled?: boolean }>(doc: T): Omit<T, '_id' | 'enabled'> {
|
|
32
|
+
const { _id, enabled, ...rest } = doc;
|
|
33
|
+
return rest;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@Injectable()
|
|
37
|
+
export class InMemoryHttpPathRepository extends HttpPathRepository {
|
|
38
|
+
private readonly col = new InMemoryCollection<StoredHttpPath>();
|
|
39
|
+
|
|
40
|
+
async insert(model: StoredHttpPath): Promise<StoredHttpPath> { return this.col.insert(model); }
|
|
41
|
+
async findById(id: string): Promise<StoredHttpPath> { return this.col.findById(id)!; }
|
|
42
|
+
async findOne(filter: Record<string, any>): Promise<StoredHttpPath> { return this.col.findOne(filter)!; }
|
|
43
|
+
async updateById(id: string, model: StoredHttpPath): Promise<StoredHttpPath> { return this.col.updateById(id, model)!; }
|
|
44
|
+
async removeById(id: string): Promise<StoredHttpPath> { return this.col.removeById(id)!; }
|
|
45
|
+
async filterPaginated(filter: Record<string, any>, page?: number, limit?: number): Promise<PaginationModel<StoredHttpPath>> {
|
|
46
|
+
return this.col.paginate(filter, Number(page) || 1, Number(limit) || 10);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async listEnabled(): Promise<PathDefinition[]> {
|
|
50
|
+
return this.col.all()
|
|
51
|
+
.filter((p) => p.enabled !== false)
|
|
52
|
+
.map((p) => toPlain(p) as PathDefinition);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async filter(filter: Record<string, any>): Promise<StoredHttpPath[]> {
|
|
56
|
+
return this.col.filter(filter);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@Injectable()
|
|
61
|
+
export class InMemoryAuthProviderRepository extends AuthProviderRepository {
|
|
62
|
+
// Storage keeps an internal _id (the collection's key); the public StoredAuthProvider has none.
|
|
63
|
+
private readonly col = new InMemoryCollection<StoredAuthProvider & { _id?: string }>();
|
|
64
|
+
|
|
65
|
+
async insert(model: StoredAuthProvider): Promise<StoredAuthProvider> { return this.col.insert(model); }
|
|
66
|
+
async findByName(name: string): Promise<StoredAuthProvider> { return this.col.findOne({ name })!; }
|
|
67
|
+
async findOne(filter: Record<string, any>): Promise<StoredAuthProvider> { return this.col.findOne(filter)!; }
|
|
68
|
+
async upsertByName(name: string, model: StoredAuthProvider): Promise<StoredAuthProvider> { return this.col.upsertOne({ name }, model); }
|
|
69
|
+
async removeByName(name: string): Promise<StoredAuthProvider> { return this.col.removeOne({ name })!; }
|
|
70
|
+
async filterPaginated(filter: Record<string, any>, page?: number, limit?: number): Promise<PaginationModel<StoredAuthProvider>> {
|
|
71
|
+
return this.col.paginate(filter, Number(page) || 1, Number(limit) || 10);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async listEnabled(): Promise<HandlerAuthConfig[]> {
|
|
75
|
+
return this.col.all()
|
|
76
|
+
.filter((p) => p.enabled !== false)
|
|
77
|
+
.map((p) => toPlain(p) as HandlerAuthConfig);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@Injectable()
|
|
82
|
+
export class InMemoryHttpMetricRepository extends HttpMetricRepository {
|
|
83
|
+
private readonly col = new InMemoryCollection<HttpMetric>();
|
|
84
|
+
private readonly pointsCol = new InMemoryCollection<HttpMetricPoint>();
|
|
85
|
+
|
|
86
|
+
async increment(input: TrackCallInput): Promise<void> {
|
|
87
|
+
if (!input?.method || !input?.route) return;
|
|
88
|
+
const existing = this.col.findOne({ method: input.method, route: input.route });
|
|
89
|
+
const isError = (input.status ?? 0) >= 400;
|
|
90
|
+
if (!existing) {
|
|
91
|
+
this.col.insert({
|
|
92
|
+
method: input.method,
|
|
93
|
+
route: input.route,
|
|
94
|
+
name: input.name,
|
|
95
|
+
topic: input.topic,
|
|
96
|
+
action: input.action,
|
|
97
|
+
count: 1,
|
|
98
|
+
errorCount: isError ? 1 : 0,
|
|
99
|
+
lastStatus: input.status,
|
|
100
|
+
lastCalledAt: Date.now(),
|
|
101
|
+
totalDurationMs: input.durationMs || 0,
|
|
102
|
+
});
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
this.col.updateById(existing._id!, {
|
|
106
|
+
name: input.name ?? existing.name,
|
|
107
|
+
topic: input.topic ?? existing.topic,
|
|
108
|
+
action: input.action ?? existing.action,
|
|
109
|
+
count: existing.count + 1,
|
|
110
|
+
errorCount: existing.errorCount + (isError ? 1 : 0),
|
|
111
|
+
lastStatus: input.status,
|
|
112
|
+
lastCalledAt: Date.now(),
|
|
113
|
+
totalDurationMs: existing.totalDurationMs + (input.durationMs || 0),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async list(route?: string): Promise<(HttpMetric & { avgDurationMs: number; })[]> {
|
|
118
|
+
const rows = route ? this.col.filter({ route }) : this.col.all();
|
|
119
|
+
return rows.map((m) => ({ ...m, avgDurationMs: m.count > 0 ? Math.round(m.totalDurationMs / m.count) : 0 }));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async record(point: HttpMetricPoint): Promise<void> {
|
|
123
|
+
if (!point?.method || !point?.route) return;
|
|
124
|
+
this.pointsCol.insert({ ...point, ts: point.ts ?? Date.now() });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async points(query: MetricQuery): Promise<HttpMetricPoint[]> {
|
|
128
|
+
const rows = this.pointsCol.all().filter((p) => matchPoint(p, query)).sort((a, b) => b.ts - a.ts);
|
|
129
|
+
return query.limit ? rows.slice(0, query.limit) : rows;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async series(query: MetricSeriesQuery): Promise<MetricSeriesBucket[]> {
|
|
133
|
+
const width = query.bucketMs > 0 ? query.bucketMs : 60_000;
|
|
134
|
+
const buckets = new Map<number, MetricSeriesBucket>();
|
|
135
|
+
for (const p of this.pointsCol.all()) {
|
|
136
|
+
if (!matchPoint(p, query)) continue;
|
|
137
|
+
const start = Math.floor(p.ts / width) * width;
|
|
138
|
+
let b = buckets.get(start);
|
|
139
|
+
if (!b) { b = { bucketStart: start, count: 0, errorCount: 0, totalDurationMs: 0, avgDurationMs: 0 }; buckets.set(start, b); }
|
|
140
|
+
b.count++;
|
|
141
|
+
if ((p.status ?? 0) >= 400) b.errorCount++;
|
|
142
|
+
const d = p.durationMs ?? 0;
|
|
143
|
+
b.totalDurationMs += d;
|
|
144
|
+
b.minDurationMs = b.minDurationMs == null ? d : Math.min(b.minDurationMs, d);
|
|
145
|
+
b.maxDurationMs = b.maxDurationMs == null ? d : Math.max(b.maxDurationMs, d);
|
|
146
|
+
}
|
|
147
|
+
const out = [...buckets.values()].sort((a, b) => a.bucketStart - b.bucketStart);
|
|
148
|
+
for (const b of out) b.avgDurationMs = b.count > 0 ? Math.round(b.totalDurationMs / b.count) : 0;
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { RouteSyncLogEntry, RouteSyncLogRepository } from '@open-rlb/nestjs-amqp';
|
|
3
|
+
import { InMemoryCollection } from './in-memory-collection';
|
|
4
|
+
|
|
5
|
+
/** In-RAM journal collection for route-sync events (added/updated/removed/collision/...). */
|
|
6
|
+
@Injectable()
|
|
7
|
+
export class InMemoryRouteSyncLogRepository extends RouteSyncLogRepository {
|
|
8
|
+
private readonly col = new InMemoryCollection<RouteSyncLogEntry>();
|
|
9
|
+
|
|
10
|
+
async insert(entry: RouteSyncLogEntry): Promise<RouteSyncLogEntry> { return this.col.insert(entry); }
|
|
11
|
+
async list(limit = 100): Promise<RouteSyncLogEntry[]> {
|
|
12
|
+
// newest first
|
|
13
|
+
return this.col.all().sort((a, b) => (b.ts || 0) - (a.ts || 0)).slice(0, limit);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: rlb-amqp
|
|
3
|
-
description: Reference, schema and gotchas for the @open-rlb/nestjs-amqp library (NestJS + RabbitMQ/AMQP + HTTP/WebSocket gateway). Use when answering questions about its YAML config (broker, topics, auth-providers, gateway
|
|
3
|
+
description: Reference, schema and gotchas for the @open-rlb/nestjs-amqp library (NestJS + RabbitMQ/AMQP + HTTP/WebSocket gateway). Use when answering questions about its YAML config (broker incl. routeDiscovery, topics, auth-providers, gateway paths/ws/events), AMQP rpc/handle/broadcast/event semantics, the @BrokerAction/@BrokerParam/@BrokerHTTP decorators, the BrokerService API, route auto-discovery, gateway-admin (routes/auth-providers/metrics/health), the name-keyed ACL, or when debugging wiring/timeout/auth/websocket errors. Shared knowledge base for the rlb-amqp-add-action, rlb-amqp-add-route, rlb-amqp-add-ws-event, rlb-amqp-scaffold, rlb-amqp-acl and rlb-amqp-gateway-admin skills.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# @open-rlb/nestjs-amqp — reference
|
|
@@ -23,17 +23,32 @@ HTTP/WS → Gateway (gateway.paths / gateway.events) → topic+action → Rabbit
|
|
|
23
23
|
topic dispatches by `action`.
|
|
24
24
|
- The same method serves both **RPC** (`requestData`, waits for the reply) and **event**
|
|
25
25
|
(`publishMessage`, waits only for the broker's publisher confirm).
|
|
26
|
+
- A microservice can announce its `@BrokerHTTP` routes to a gateway over AMQP
|
|
27
|
+
(**route auto-discovery**) so the gateway registers them without YAML edits.
|
|
26
28
|
|
|
27
29
|
## When to use this skill
|
|
28
30
|
|
|
29
31
|
Load the bundled reference files before editing config, handlers or the gateway:
|
|
30
32
|
|
|
31
|
-
- **`references/config-schema.md`** — full YAML schema for every section, field by field
|
|
33
|
+
- **`references/config-schema.md`** — full YAML schema for every section, field by field
|
|
34
|
+
(broker incl. `routeDiscovery`, topics 4 modes, auth-providers name-keyed, gateway
|
|
35
|
+
paths + ws + events).
|
|
32
36
|
- **`references/gotchas.md`** — the checklist of bug-prone cases. ALWAYS scan it before
|
|
33
37
|
adding/changing a topic, queue, exchange, action, route, auth provider or WS event.
|
|
34
38
|
|
|
35
|
-
The human-facing docs live
|
|
36
|
-
rules-first version for editing tasks
|
|
39
|
+
The authoritative human-facing docs live under the repo `docs/` directory; these two files
|
|
40
|
+
are the terse, rules-first version for editing tasks:
|
|
41
|
+
|
|
42
|
+
- `docs/README.md` — index. `docs/getting-started.md` — bootstrap a ms + gateway.
|
|
43
|
+
- `docs/broker.md` — `@BrokerAction`/`@BrokerParam`, topic modes, RPC, `BrokerService`.
|
|
44
|
+
- `docs/gateway.md` — `gateway.paths[]`, `gateway.events[]`, auth gate, status mapping, ws.
|
|
45
|
+
- `docs/acl.md` — name-keyed actions/roles, dual grant/revoke, `acl-can-user-do*` checks.
|
|
46
|
+
- `docs/gateway-admin.md` — DB routes, name-keyed auth-providers, metrics, health, route-sync.
|
|
47
|
+
- `docs/gotchas.md` — the canonical source `references/gotchas.md` is ported from.
|
|
48
|
+
|
|
49
|
+
Runnable examples live under `sample/config-sample/` (`gateway-in-memory`, `gateway-db`,
|
|
50
|
+
`calculator.ms`, plus the annotated `broker/gateway/acl/gateway-admin.yaml` reference
|
|
51
|
+
configs). The retired `apps/gateway-2` is gone — do not cite it.
|
|
37
52
|
|
|
38
53
|
## Sibling task skills
|
|
39
54
|
|
|
@@ -41,6 +56,8 @@ rules-first version for editing tasks.
|
|
|
41
56
|
- `rlb-amqp-add-route` — expose an action over HTTP (`gateway.paths[]`).
|
|
42
57
|
- `rlb-amqp-add-ws-event` — add a secure WebSocket/webhook event (`gateway.events[]`).
|
|
43
58
|
- `rlb-amqp-scaffold` — bootstrap a new microservice/gateway (module, main, config).
|
|
59
|
+
- `rlb-amqp-acl` — name-keyed actions/roles, grant/revoke, the `acl-can-user-do*` checks.
|
|
60
|
+
- `rlb-amqp-gateway-admin` — DB routes/auth-providers, metrics, health, route auto-discovery.
|
|
44
61
|
|
|
45
62
|
## Golden rules (summary — full list in references/gotchas.md)
|
|
46
63
|
|
|
@@ -49,7 +66,8 @@ rules-first version for editing tasks.
|
|
|
49
66
|
2. `mode: rpc`/`handle` need `topics[].queue` present in `broker.queues[]`, whose
|
|
50
67
|
`exchange` exists in `broker.exchanges[]`.
|
|
51
68
|
3. Exchange `type: topic` → the queue MUST have a `routingKey`.
|
|
52
|
-
4. `broadcast` and the WebSocket gateway require `connection_name
|
|
69
|
+
4. `broadcast` and the WebSocket gateway require a **distinct** `connection_name` per
|
|
70
|
+
instance (set it, or let `broker.routeDiscovery.serviceName` fill it in).
|
|
53
71
|
5. `(topic, action)` must be unique — duplicates overwrite silently.
|
|
54
72
|
6. No destructuring / default values in `@BrokerAction` method parameters; always pass an
|
|
55
73
|
explicit `name` to `@BrokerParam`.
|
|
@@ -57,3 +75,10 @@ rules-first version for editing tasks.
|
|
|
57
75
|
8. `roles` (HTTP or WS) require an `IAclRoleService` registered via
|
|
58
76
|
`RLB_GTW_ACL_ROLE_SERVICE` in `ProxyModule.forRootAsync({ providers: [...] })`.
|
|
59
77
|
Auth-providers + gateway config are passed to `ProxyModule` (not `BrokerModule`).
|
|
78
|
+
9. Topic names `rlb-acl` / `rlb-gateway-admin` / `rlb-gateway-control` and ALL action
|
|
79
|
+
strings (`acl-*`, `gw-path-*`, `gw-auth-*`, `gw-metrics-*`, `gw-health`, `gw-reload`)
|
|
80
|
+
are decorator-bound and NOT configurable. Only exchange/queue/routingKey and the
|
|
81
|
+
route-discovery exchange/queue are.
|
|
82
|
+
10. ACL actions/roles and gateway-admin auth-providers are **name-keyed**: `PUT` upserts,
|
|
83
|
+
`GET` lists, `GET .../get?name=`, `DELETE` by name. There is no POST and no id-based
|
|
84
|
+
ACL CRUD. Boolean checks (`/acl/check*`) return `200` with `true`/`false`.
|