@dudousxd/nestjs-catalog 0.2.0 → 0.3.0

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 CHANGED
@@ -84,6 +84,46 @@ overlay store (a JSON file by default; bring your own via `overlayStore`).
84
84
  searched columns against the catalog before anything reaches SQL, selects only
85
85
  the columns the catalog says are visible, and caps the page size.
86
86
 
87
+ ## Who may reach it
88
+
89
+ The console's Access screen asks two questions with different owners, and the
90
+ library treats them differently on purpose.
91
+
92
+ **Applications** are the catalog's own. `catalog_principal` is a table this
93
+ library defines and the grants on it are catalog grants, so
94
+ `CatalogMikroOrmStoreModule` ships the implementation and you get
95
+ `GET /access/principals` by mounting it.
96
+
97
+ **People are almost certainly not the catalog's.** A catalog embedded in an
98
+ application is embedded in one that already knows who its users are, and a
99
+ second user store beside it is how you get two lists of employees that disagree
100
+ about who was offboarded. So implement `listPeople` over what you already have:
101
+
102
+ ```ts
103
+ class MyDirectory extends MikroOrmCatalogDirectory { // applications, inherited
104
+ async listPeople() { return this.users.findAll().map(toPersonSummary); }
105
+ }
106
+
107
+ CatalogModule.forRoot({
108
+ directory: { provide: CATALOG_DIRECTORY, useClass: MyDirectory },
109
+ });
110
+ ```
111
+
112
+ Bind it through `directory` rather than only exporting it from an imported
113
+ module: a provider declared inside `CatalogModule` **shadows** the same token
114
+ exported by one of its imports, so a host that does both gets the shipped
115
+ applications-only one and no error.
116
+
117
+ Leave `listPeople` out and `GET /access/people` answers **501** naming the seam,
118
+ rather than an empty list. That distinction is load-bearing — "nobody can sign in
119
+ yet" and "we did not ask" send an operator to different places, and the first
120
+ invites them to create an account that already exists. Same for `upsertPerson`,
121
+ which most hosts should *not* implement: creating a user from a catalog console
122
+ is a way to create one your IdP has never heard of.
123
+
124
+ The routes mount at `accessPath`, a sibling of `path` by default — `api/catalog`
125
+ gives `api/access`, which is the shape the React screens build.
126
+
87
127
  ## Build your own endpoints, or use ours
88
128
 
89
129
  The built-in controller is a convenience, not the interface. Pass
@@ -0,0 +1,10 @@
1
+ import { type Type } from '@nestjs/common';
2
+ /**
3
+ * The Access screen's endpoints.
4
+ *
5
+ * A factory for the same reason as `createCatalogController`: the prefix and
6
+ * the guards come from `forRoot`, and this surface enumerates every application
7
+ * that can write to the catalog, which is not something to hand a library's
8
+ * default opinion about auth.
9
+ */
10
+ export declare function createAccessController(path: string, guards: Type<unknown>[], decorators?: ClassDecorator[]): Type<unknown>;
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.createAccessController = createAccessController;
16
+ const common_1 = require("@nestjs/common");
17
+ const catalog_access_1 = require("./catalog.access");
18
+ const ROLES = ['viewer', 'curator', 'administrator'];
19
+ function isRole(value) {
20
+ return typeof value === 'string' && ROLES.some((role) => role === value);
21
+ }
22
+ /**
23
+ * The Access screen's endpoints.
24
+ *
25
+ * A factory for the same reason as `createCatalogController`: the prefix and
26
+ * the guards come from `forRoot`, and this surface enumerates every application
27
+ * that can write to the catalog, which is not something to hand a library's
28
+ * default opinion about auth.
29
+ */
30
+ function createAccessController(path, guards, decorators = []) {
31
+ let AccessController = class AccessController {
32
+ directory;
33
+ constructor(directory) {
34
+ this.directory = directory;
35
+ }
36
+ require() {
37
+ if (!this.directory) {
38
+ throw new common_1.NotImplementedException('This catalog has no directory, so it cannot say who may reach it. ' +
39
+ 'Provide CATALOG_DIRECTORY — `CatalogMikroOrmStoreModule` ships one that reads `catalog_principal`.');
40
+ }
41
+ return this.directory;
42
+ }
43
+ principals() {
44
+ return this.require().listApplications();
45
+ }
46
+ people(search, limit, offset) {
47
+ const directory = this.require();
48
+ if (!directory.listPeople) {
49
+ // 501, not 200-with-nothing. The screen renders a failed read and an
50
+ // empty list very differently on purpose, and this is genuinely the
51
+ // first: the people exist, they are just not ours to enumerate.
52
+ throw new common_1.NotImplementedException("People come from this application's own identity system, not from the catalog. " +
53
+ 'Implement `listPeople` on CATALOG_DIRECTORY to list them here.');
54
+ }
55
+ // Bounded HERE, not left to each implementation. A host's user table is
56
+ // its whole directory, and the seam would otherwise invite exactly the
57
+ // unbounded read this endpoint would then serve to a browser.
58
+ return directory.listPeople({
59
+ ...(search ? { search } : {}),
60
+ limit: boundedPage(limit),
61
+ offset: nonNegative(offset),
62
+ });
63
+ }
64
+ upsertPerson(body) {
65
+ const directory = this.require();
66
+ if (!directory.upsertPerson) {
67
+ throw new common_1.NotImplementedException("This catalog's directory is read-only. Add or change people where they actually live; " +
68
+ 'implement `upsertPerson` on CATALOG_DIRECTORY only if that is here.');
69
+ }
70
+ return directory.upsertPerson(parsePersonInput(body));
71
+ }
72
+ };
73
+ __decorate([
74
+ (0, common_1.Get)('principals'),
75
+ __metadata("design:type", Function),
76
+ __metadata("design:paramtypes", []),
77
+ __metadata("design:returntype", void 0)
78
+ ], AccessController.prototype, "principals", null);
79
+ __decorate([
80
+ (0, common_1.Get)('people'),
81
+ __param(0, (0, common_1.Query)('search')),
82
+ __param(1, (0, common_1.Query)('limit')),
83
+ __param(2, (0, common_1.Query)('offset')),
84
+ __metadata("design:type", Function),
85
+ __metadata("design:paramtypes", [String, String, String]),
86
+ __metadata("design:returntype", void 0)
87
+ ], AccessController.prototype, "people", null);
88
+ __decorate([
89
+ (0, common_1.Post)('people'),
90
+ __param(0, (0, common_1.Body)()),
91
+ __metadata("design:type", Function),
92
+ __metadata("design:paramtypes", [Object]),
93
+ __metadata("design:returntype", void 0)
94
+ ], AccessController.prototype, "upsertPerson", null);
95
+ AccessController = __decorate([
96
+ (0, common_1.Controller)(path),
97
+ __param(0, (0, common_1.Optional)()),
98
+ __param(0, (0, common_1.Inject)(catalog_access_1.CATALOG_DIRECTORY)),
99
+ __metadata("design:paramtypes", [Object])
100
+ ], AccessController);
101
+ if (guards.length > 0)
102
+ (0, common_1.UseGuards)(...guards)(AccessController);
103
+ for (const decorate of decorators)
104
+ decorate(AccessController);
105
+ return AccessController;
106
+ }
107
+ /**
108
+ * A page size that cannot be talked out of its ceiling.
109
+ *
110
+ * Anything unparseable falls back to the default rather than to "no limit" —
111
+ * `?limit=all` must not be the one string that turns the bound off.
112
+ */
113
+ function boundedPage(raw) {
114
+ const asked = Number.parseInt(raw ?? '', 10);
115
+ if (!Number.isFinite(asked) || asked <= 0)
116
+ return catalog_access_1.CATALOG_DIRECTORY_PAGE;
117
+ return Math.min(asked, catalog_access_1.CATALOG_DIRECTORY_MAX_PAGE);
118
+ }
119
+ function nonNegative(raw) {
120
+ const asked = Number.parseInt(raw ?? '', 10);
121
+ return Number.isFinite(asked) && asked > 0 ? asked : 0;
122
+ }
123
+ /**
124
+ * Validated here rather than with a DTO class, because this package ships no
125
+ * `class-validator` dependency and a host's global `ValidationPipe` is not
126
+ * something a library can assume is mounted.
127
+ */
128
+ function parsePersonInput(body) {
129
+ if (!body || typeof body !== 'object') {
130
+ throw new common_1.BadRequestException('Expected an object describing the person.');
131
+ }
132
+ const email = Reflect.get(body, 'email');
133
+ if (typeof email !== 'string' || !email.includes('@')) {
134
+ throw new common_1.BadRequestException('`email` is required and must be an email address.');
135
+ }
136
+ const role = Reflect.get(body, 'role');
137
+ if (!isRole(role)) {
138
+ throw new common_1.BadRequestException(`\`role\` must be one of: ${ROLES.join(', ')}.`);
139
+ }
140
+ const displayName = Reflect.get(body, 'displayName');
141
+ const active = Reflect.get(body, 'active');
142
+ const password = Reflect.get(body, 'password');
143
+ return {
144
+ email,
145
+ role,
146
+ ...(typeof displayName === 'string' ? { displayName } : {}),
147
+ ...(typeof active === 'boolean' ? { active } : {}),
148
+ ...(typeof password === 'string' ? { password } : {}),
149
+ };
150
+ }
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Where the Access screen's two lists come from.
3
+ *
4
+ * The screen answers one question — "which application is allowed to touch
5
+ * what, and which person is behind it" — but the two halves of that answer have
6
+ * very different owners.
7
+ *
8
+ * *Applications* are the catalog's own. `catalog_principal` is a table this
9
+ * library defines, the grants on it are catalog grants, and no host has a
10
+ * pre-existing notion of "may load the `Vehicle` type". The library can and does
11
+ * serve this half itself.
12
+ *
13
+ * *People* are almost never the catalog's. A catalog embedded in an application
14
+ * is embedded in an application that already knows who its users are — an IdP, a
15
+ * session table, an OIDC provider — and standing up a second user store beside
16
+ * it is how you get two lists of employees that disagree about who was
17
+ * offboarded. So this half is a seam: the host implements it over whatever it
18
+ * already has, or it does not implement it and the screen says so.
19
+ *
20
+ * That asymmetry is why `listPeople` and `upsertPerson` are OPTIONAL while
21
+ * `listApplications` is not. A directory that only knows about applications is a
22
+ * complete, expected implementation, not a half-finished one.
23
+ */
24
+ /** A calling application, as the Access screen needs it. */
25
+ export interface CatalogApplicationSummary {
26
+ id: string;
27
+ displayName: string;
28
+ scopes: string[];
29
+ writeTypes: string[];
30
+ /** Null means every type — a safe default for read, unlike write. */
31
+ readTypes: string[] | null;
32
+ classifications: string[];
33
+ active: boolean;
34
+ /**
35
+ * How it authenticates. Never the credential itself — a console that can
36
+ * display one is a console that leaks it over somebody's shoulder.
37
+ */
38
+ authMethod: 'key' | 'token' | 'session';
39
+ lastSeenAt: string | null;
40
+ /**
41
+ * Types whose publishing this application owns.
42
+ *
43
+ * Distinct from `writeTypes`, which is the grant. Ownership is what actually
44
+ * decides a re-publish, so a screen showing only the grant can show an
45
+ * application that appears able to load a type it will be refused on.
46
+ */
47
+ ownedTypes: string[];
48
+ }
49
+ export type CatalogPersonRole = 'viewer' | 'curator' | 'administrator';
50
+ /** A person: signs in, and acts through an application that caps them. */
51
+ export interface CatalogPersonSummary {
52
+ email: string;
53
+ displayName: string;
54
+ role: CatalogPersonRole;
55
+ active: boolean;
56
+ /** Exactly the string their actions land in the audit trail as. */
57
+ principalId: string;
58
+ /**
59
+ * Whether they can sign in with a password *here*.
60
+ *
61
+ * False is the normal answer for a host-backed directory, where signing in
62
+ * happens somewhere this library never sees. It is not "no credential".
63
+ */
64
+ hasPassword: boolean;
65
+ createdAt: string;
66
+ lastLoginAt: string | null;
67
+ /** Sessions the host can see. `0` from a directory that does not track them. */
68
+ liveSessions: number;
69
+ /**
70
+ * The intersection with the application they sign in through, not the role in
71
+ * the abstract. An administrator whose console has had `catalog:admin`
72
+ * removed is not an administrator, and showing the role alone would say
73
+ * otherwise.
74
+ */
75
+ effective: {
76
+ scopes: string[];
77
+ writeTypes: string[];
78
+ readTypes: string[] | null;
79
+ classifications: string[];
80
+ };
81
+ }
82
+ /** The bound the endpoint applies before the directory is asked. */
83
+ export interface CatalogDirectoryQuery {
84
+ /**
85
+ * Free text over whatever identifies a person to the host — email and display
86
+ * name, typically. Absent means no filter, NOT "match nothing".
87
+ */
88
+ search?: string;
89
+ /** Always present, always capped. See {@link CATALOG_DIRECTORY_MAX_PAGE}. */
90
+ limit: number;
91
+ offset: number;
92
+ }
93
+ /** The default page, when the caller asks for no particular one. */
94
+ export declare const CATALOG_DIRECTORY_PAGE = 50;
95
+ /**
96
+ * The ceiling, whatever a caller asks for.
97
+ *
98
+ * A cap rather than a default that can be overridden without limit, because the
99
+ * caller here is a browser and `?limit=100000` would be one URL away from the
100
+ * unbounded read this exists to prevent.
101
+ */
102
+ export declare const CATALOG_DIRECTORY_MAX_PAGE = 500;
103
+ export interface CatalogPeoplePage {
104
+ people: CatalogPersonSummary[];
105
+ /**
106
+ * How many match, ignoring the page. The console needs it to say "50 of 1,340"
107
+ * — a bounded list that cannot report what it is bounding is indistinguishable
108
+ * from a complete one, and an operator reading it as complete will conclude
109
+ * somebody has no access when they simply were not on the page.
110
+ */
111
+ total: number;
112
+ limit: number;
113
+ offset: number;
114
+ }
115
+ export interface CatalogPersonInput {
116
+ email: string;
117
+ displayName?: string;
118
+ role: CatalogPersonRole;
119
+ active?: boolean;
120
+ password?: string;
121
+ }
122
+ export interface CatalogPersonUpsertResult {
123
+ email: string;
124
+ created: boolean;
125
+ /**
126
+ * Whether open sessions were invalidated. Surfaced because demoting somebody
127
+ * whose tabs keep working is the failure that makes people distrust the whole
128
+ * session system.
129
+ */
130
+ sessionsRevoked: boolean;
131
+ }
132
+ /**
133
+ * The host's answer to "who may reach this catalog".
134
+ *
135
+ * Implement `listPeople` over the user store you already have. The three
136
+ * fields worth care are `principalId` — it must be the exact string that
137
+ * person's actions are recorded under, or the Activity screen attributes their
138
+ * work to nobody — `role`, and `effective`, which is the role capped by the
139
+ * application they sign in through rather than the role in the abstract.
140
+ */
141
+ export interface CatalogDirectory {
142
+ /**
143
+ * Unpaged, and that is a claim about the data rather than an oversight:
144
+ * applications are rows an operator creates by hand, one per publisher, and a
145
+ * catalog with a thousand of them has a different problem than a missing
146
+ * `LIMIT`. People are the opposite — see {@link listPeople}.
147
+ */
148
+ listApplications(): Promise<CatalogApplicationSummary[]>;
149
+ /**
150
+ * Omit when people come from somewhere this catalog cannot see. The endpoint
151
+ * then refuses with a message naming this seam, which is a better answer than
152
+ * an empty list: "nobody can sign in yet" and "we did not ask" send an
153
+ * operator to completely different places, and the first invites them to
154
+ * create an account that already exists.
155
+ *
156
+ * **The query is not advisory.** It is passed so the bound reaches the
157
+ * database, not so the implementation can slice an array it already
158
+ * materialised — an embedded catalog's user table is the host's whole
159
+ * directory, and `SELECT *` over it is a screen that gets slower every time
160
+ * somebody is hired. `total` is what lets the console say how much it is not
161
+ * showing, which is the difference between a bounded list and a silently
162
+ * truncated one.
163
+ */
164
+ listPeople?(query: CatalogDirectoryQuery): Promise<CatalogPeoplePage>;
165
+ /**
166
+ * Omit when the host's directory is not writable from here — which is the
167
+ * common case, and the right one. Creating a user in the IdP from a catalog
168
+ * console is a way to create a user the IdP does not know about.
169
+ */
170
+ upsertPerson?(input: CatalogPersonInput): Promise<CatalogPersonUpsertResult>;
171
+ }
172
+ export declare const CATALOG_DIRECTORY: unique symbol;
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ /**
3
+ * Where the Access screen's two lists come from.
4
+ *
5
+ * The screen answers one question — "which application is allowed to touch
6
+ * what, and which person is behind it" — but the two halves of that answer have
7
+ * very different owners.
8
+ *
9
+ * *Applications* are the catalog's own. `catalog_principal` is a table this
10
+ * library defines, the grants on it are catalog grants, and no host has a
11
+ * pre-existing notion of "may load the `Vehicle` type". The library can and does
12
+ * serve this half itself.
13
+ *
14
+ * *People* are almost never the catalog's. A catalog embedded in an application
15
+ * is embedded in an application that already knows who its users are — an IdP, a
16
+ * session table, an OIDC provider — and standing up a second user store beside
17
+ * it is how you get two lists of employees that disagree about who was
18
+ * offboarded. So this half is a seam: the host implements it over whatever it
19
+ * already has, or it does not implement it and the screen says so.
20
+ *
21
+ * That asymmetry is why `listPeople` and `upsertPerson` are OPTIONAL while
22
+ * `listApplications` is not. A directory that only knows about applications is a
23
+ * complete, expected implementation, not a half-finished one.
24
+ */
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.CATALOG_DIRECTORY = exports.CATALOG_DIRECTORY_MAX_PAGE = exports.CATALOG_DIRECTORY_PAGE = void 0;
27
+ /** The default page, when the caller asks for no particular one. */
28
+ exports.CATALOG_DIRECTORY_PAGE = 50;
29
+ /**
30
+ * The ceiling, whatever a caller asks for.
31
+ *
32
+ * A cap rather than a default that can be overridden without limit, because the
33
+ * caller here is a browser and `?limit=100000` would be one URL away from the
34
+ * unbounded read this exists to prevent.
35
+ */
36
+ exports.CATALOG_DIRECTORY_MAX_PAGE = 500;
37
+ exports.CATALOG_DIRECTORY = Symbol('CATALOG_DIRECTORY');
@@ -10,6 +10,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.CatalogModule = void 0;
11
11
  const node_path_1 = require("node:path");
12
12
  const common_1 = require("@nestjs/common");
13
+ const access_controller_1 = require("./access.controller");
13
14
  const catalog_controller_1 = require("./catalog.controller");
14
15
  const catalog_options_1 = require("./catalog.options");
15
16
  const catalog_overlay_store_1 = require("./catalog.overlay-store");
@@ -24,7 +25,10 @@ let CatalogModule = CatalogModule_1 = class CatalogModule {
24
25
  const path = options.path ?? 'api/catalog';
25
26
  const mountController = options.controller !== false;
26
27
  const controllers = mountController
27
- ? [(0, catalog_controller_1.createCatalogController)(path, options.guards ?? [], options.decorators ?? [])]
28
+ ? [
29
+ (0, catalog_controller_1.createCatalogController)(path, options.guards ?? [], options.decorators ?? []),
30
+ (0, access_controller_1.createAccessController)(options.accessPath ?? siblingPath(path, 'access'), options.guards ?? [], options.decorators ?? []),
31
+ ]
28
32
  : [];
29
33
  const overlayStore = options.overlayStore ??
30
34
  new catalog_overlay_store_1.FileCatalogOverlayStore(options.overlayPath ?? (0, node_path_1.join)(process.cwd(), '.catalog', 'overlay.json'));
@@ -59,6 +63,11 @@ let CatalogModule = CatalogModule_1 = class CatalogModule {
59
63
  ...(options.store
60
64
  ? [options.store]
61
65
  : [mikro_orm_read_store_1.MikroOrmReadStore, { provide: catalog_store_1.CATALOG_STORE, useExisting: mikro_orm_read_store_1.MikroOrmReadStore }]),
66
+ // No default. Unlike the store and the registry there is nothing
67
+ // sensible to derive: who may reach a catalog is not a fact about its
68
+ // model. Absent, the Access routes answer 501 naming the token, which
69
+ // is the honest state for a host that has not decided yet.
70
+ ...(options.directory ? [options.directory] : []),
62
71
  catalog_service_1.CatalogService,
63
72
  ],
64
73
  exports: [catalog_registry_base_1.CatalogRegistry, catalog_service_1.CatalogService, catalog_store_1.CATALOG_STORE],
@@ -69,3 +78,16 @@ exports.CatalogModule = CatalogModule;
69
78
  exports.CatalogModule = CatalogModule = CatalogModule_1 = __decorate([
70
79
  (0, common_1.Module)({})
71
80
  ], CatalogModule);
81
+ /**
82
+ * `api/catalog` + `access` -> `api/access`.
83
+ *
84
+ * Replacing the last segment rather than appending, because the console builds
85
+ * the two paths as siblings under one API root. Appending would give
86
+ * `api/catalog/access`, which the console never asks for — and the symptom is a
87
+ * screen that 404s while the module reports itself mounted.
88
+ */
89
+ function siblingPath(path, segment) {
90
+ const trimmed = path.replace(/\/+$/, '');
91
+ const parent = trimmed.slice(0, trimmed.lastIndexOf('/') + 1);
92
+ return `${parent}${segment}`;
93
+ }
@@ -16,6 +16,21 @@ export interface CatalogModuleOptions {
16
16
  * Defaults to `api/catalog`.
17
17
  */
18
18
  path?: string;
19
+ /**
20
+ * Route prefix for the Access endpoints, which the console's Access screen
21
+ * reads.
22
+ *
23
+ * A sibling of {@link path} by default — `api/catalog` gives `api/access` —
24
+ * because that is the shape `@dudousxd/nestjs-catalog-react` builds by
25
+ * default: it prepends the console's `apiPath` to a base of `/access`, so
26
+ * the two land side by side under the same API root.
27
+ *
28
+ * The endpoints are served only when something provides `CATALOG_DIRECTORY`;
29
+ * see {@link CatalogDirectory}. Mounted regardless, so the console gets a
30
+ * 501 explaining what is unwired rather than a 404 that reads as a broken
31
+ * build.
32
+ */
33
+ accessPath?: string;
19
34
  /**
20
35
  * Guards applied to every catalog route. The library ships none: an
21
36
  * introspection endpoint that lists every table in the system is exactly the
@@ -55,6 +70,20 @@ export interface CatalogModuleOptions {
55
70
  * implementation here instead.
56
71
  */
57
72
  registry?: Provider;
73
+ /**
74
+ * Who may reach this catalog. A provider for `CATALOG_DIRECTORY`.
75
+ *
76
+ * Bound here rather than only exported from an imported module for the same
77
+ * reason `store` and `registry` are: a provider declared inside this module
78
+ * SHADOWS the same token exported by one of its imports, so a host that
79
+ * merely imports `CatalogMikroOrmStoreModule` alongside its own
80
+ * implementation gets the shipped applications-only one and no error.
81
+ *
82
+ * The common shape is to extend the shipped `MikroOrmCatalogDirectory` —
83
+ * which reads `catalog_principal` — and add `listPeople` over the user store
84
+ * the host already has. See {@link CatalogDirectory}.
85
+ */
86
+ directory?: Provider;
58
87
  /**
59
88
  * Mount the built-in controller. Default true.
60
89
  *
@@ -249,7 +249,7 @@ function validateWorkflow(graph) {
249
249
  });
250
250
  continue;
251
251
  }
252
- const key = `${edge.from}${edge.to}`;
252
+ const key = `${edge.from}\0${edge.to}`;
253
253
  if (seenEdges.has(key)) {
254
254
  issues.push({
255
255
  code: 'duplicate-edge',
@@ -162,7 +162,7 @@ export declare function composePrincipalId(applicationId: string, actorId?: stri
162
162
  *
163
163
  * Total, and lenient by design: every id ever written parses, including the
164
164
  * millions written before actors existed. This is what lets a governance query
165
- * keep asking "everything `flip-nestjs` did" — it matches on `applicationId`
165
+ * keep asking "everything this application did" — it matches on `applicationId`
166
166
  * and gets both the machine's own loads and anything a person did through it.
167
167
  *
168
168
  * Splits on the *first* separator, so an actor id that somehow contains one
@@ -101,7 +101,7 @@ function composePrincipalId(applicationId, actorId) {
101
101
  *
102
102
  * Total, and lenient by design: every id ever written parses, including the
103
103
  * millions written before actors existed. This is what lets a governance query
104
- * keep asking "everything `flip-nestjs` did" — it matches on `applicationId`
104
+ * keep asking "everything this application did" — it matches on `applicationId`
105
105
  * and gets both the machine's own loads and anything a person did through it.
106
106
  *
107
107
  * Splits on the *first* separator, so an actor id that somehow contains one
package/dist/index.d.ts CHANGED
@@ -14,6 +14,7 @@ export { SubprocessTransformRunner, type TransformRunnerOptions, } from './trans
14
14
  export { CatalogService } from './catalog.service';
15
15
  export { type AuditQuery, CATALOG_TRACE_OUTCOMES, CATALOG_TRACE_STORE, CATALOG_WORKSPACE_STORE, type CatalogAuditEvent, type CatalogTrace, type CatalogTraceList, type CatalogTraceOutcome, type CatalogTraceSpan, type CatalogTraceStore, type CatalogTraceTotals, type CatalogUnlinkedList, type CatalogWorkspaceStore, type Dashboard, type DashboardCard, isCatalogTraceOutcome, isTraceStore, isWorkspaceStore, type QueryVisualization, type SaveQueryInput, type SavedQuery, type TraceQuery, traceOutcomeFilter, } from './catalog.workspace';
16
16
  export { CATALOG_PRINCIPAL_RESOLVER, type CatalogActor, type CatalogGrants, type CatalogPrincipal, type CatalogPrincipalResolver, type CatalogScope, composePrincipalId, delegatePrincipal, expandScopes, hasScope, parsePrincipalId, PRINCIPAL_ACTOR_SEPARATOR, maySeeClassification, mayRead, mayWrite, StaticKeyPrincipalResolver, } from './catalog.principal';
17
+ export { CATALOG_DIRECTORY, type CatalogApplicationSummary, type CatalogDirectory, type CatalogPersonInput, type CatalogPersonRole, type CatalogPersonSummary, type CatalogPersonUpsertResult, } from './catalog.access';
17
18
  export { assertNoColumnCollisions, CATALOG_RESERVED_COLUMNS, CATALOG_SNAPSHOT_MODES, CATALOG_STORE, type CarryForwardResult, type CatalogColumnCollision, CatalogColumnCollisionError, type CatalogMergeStore, type CatalogReadQuery, type CatalogReadResult, type CatalogReadStore, type CatalogReservedColumn, type CatalogSnapshotMode, type CatalogStoreCapabilities, type CatalogWriteStore, type ColumnCollisionOptions, findColumnCollisions, isCatalogStoreCapabilities, isReservedColumn, isWriteStore, type SnapshotRef, supportsCarryForward, } from './catalog.store';
18
19
  export { MikroOrmReadStore } from './stores/mikro-orm-read.store';
19
20
  export type { CatalogGraph, CatalogObjectPage, CatalogObjectQuery, CatalogObjectTypeDef, CatalogOverlay, CatalogPropertyDef, CatalogRelationDef, CatalogSnapshot, RelationKind, ScalarType, } from './catalog.types';
package/dist/index.js CHANGED
@@ -15,7 +15,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.CATALOG_PRINCIPAL_RESOLVER = exports.traceOutcomeFilter = exports.isWorkspaceStore = exports.isTraceStore = exports.isCatalogTraceOutcome = exports.CATALOG_WORKSPACE_STORE = exports.CATALOG_TRACE_STORE = exports.CATALOG_TRACE_OUTCOMES = exports.CatalogService = exports.SubprocessTransformRunner = exports.toCsv = exports.QueryCache = exports.workflowRunOrder = exports.workflowGraphHash = exports.WORKFLOW_NODE_KINDS = exports.WORKFLOW_NODE_ID_PATTERN = exports.WORKFLOW_ISSUE_CODES = exports.WORKFLOW_EXECUTION_MODES = exports.validateWorkflow = exports.TRANSFORM_LANGUAGES = exports.TRANSFORM_RUNNER = exports.supportsWorkflowStages = exports.supportsWorkflows = exports.isWorkflowNodeKind = exports.isWorkflowNode = exports.isWorkflowExecutionMode = exports.isWorkflowEdge = exports.isTransformLanguage = exports.isPipelineStore = exports.isConnectorKind = exports.CONNECTOR_KINDS = exports.CATALOG_PIPELINE_STORE = exports.CatalogRegistry = exports.MikroOrmCatalogRegistry = exports.CATALOG_OVERLAY_STORE = exports.InMemoryCatalogOverlayStore = exports.FileCatalogOverlayStore = exports.CATALOG_OPTIONS = exports.isQueryStore = exports.assertReadOnlyShape = exports.CatalogModule = exports.emitCatalog = exports.channelNameFor = exports.catalogEventPhase = exports.CATALOG_LIB = exports.CATALOG_EVENTS = exports.CATALOG_EVENT_PHASE_FALLBACK = exports.CATALOG_EVENT_PHASE = exports.CatalogType = exports.CatalogProperty = void 0;
18
- exports.RequireScopes = exports.RequireHuman = exports.REQUIRES_HUMAN = exports.REQUIRED_SCOPES = exports.MikroOrmReadStore = exports.supportsCarryForward = exports.isWriteStore = exports.isReservedColumn = exports.isCatalogStoreCapabilities = exports.findColumnCollisions = exports.CatalogColumnCollisionError = exports.CATALOG_STORE = exports.CATALOG_SNAPSHOT_MODES = exports.CATALOG_RESERVED_COLUMNS = exports.assertNoColumnCollisions = exports.StaticKeyPrincipalResolver = exports.mayWrite = exports.mayRead = exports.maySeeClassification = exports.PRINCIPAL_ACTOR_SEPARATOR = exports.parsePrincipalId = exports.hasScope = exports.expandScopes = exports.delegatePrincipal = exports.composePrincipalId = void 0;
18
+ exports.RequireScopes = exports.RequireHuman = exports.REQUIRES_HUMAN = exports.REQUIRED_SCOPES = exports.MikroOrmReadStore = exports.supportsCarryForward = exports.isWriteStore = exports.isReservedColumn = exports.isCatalogStoreCapabilities = exports.findColumnCollisions = exports.CatalogColumnCollisionError = exports.CATALOG_STORE = exports.CATALOG_SNAPSHOT_MODES = exports.CATALOG_RESERVED_COLUMNS = exports.assertNoColumnCollisions = exports.CATALOG_DIRECTORY = exports.StaticKeyPrincipalResolver = exports.mayWrite = exports.mayRead = exports.maySeeClassification = exports.PRINCIPAL_ACTOR_SEPARATOR = exports.parsePrincipalId = exports.hasScope = exports.expandScopes = exports.delegatePrincipal = exports.composePrincipalId = void 0;
19
19
  var catalog_decorators_1 = require("./catalog.decorators");
20
20
  Object.defineProperty(exports, "CatalogProperty", { enumerable: true, get: function () { return catalog_decorators_1.CatalogProperty; } });
21
21
  Object.defineProperty(exports, "CatalogType", { enumerable: true, get: function () { return catalog_decorators_1.CatalogType; } });
@@ -94,6 +94,8 @@ Object.defineProperty(exports, "maySeeClassification", { enumerable: true, get:
94
94
  Object.defineProperty(exports, "mayRead", { enumerable: true, get: function () { return catalog_principal_1.mayRead; } });
95
95
  Object.defineProperty(exports, "mayWrite", { enumerable: true, get: function () { return catalog_principal_1.mayWrite; } });
96
96
  Object.defineProperty(exports, "StaticKeyPrincipalResolver", { enumerable: true, get: function () { return catalog_principal_1.StaticKeyPrincipalResolver; } });
97
+ var catalog_access_1 = require("./catalog.access");
98
+ Object.defineProperty(exports, "CATALOG_DIRECTORY", { enumerable: true, get: function () { return catalog_access_1.CATALOG_DIRECTORY; } });
97
99
  var catalog_store_1 = require("./catalog.store");
98
100
  Object.defineProperty(exports, "assertNoColumnCollisions", { enumerable: true, get: function () { return catalog_store_1.assertNoColumnCollisions; } });
99
101
  Object.defineProperty(exports, "CATALOG_RESERVED_COLUMNS", { enumerable: true, get: function () { return catalog_store_1.CATALOG_RESERVED_COLUMNS; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dudousxd/nestjs-catalog",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "A metadata registry for NestJS: object types, properties and relations, derived from your ORM and enriched with decorators.",
5
5
  "license": "MIT",
6
6
  "author": "Davide Carvalho",