@astram/identity 0.16.0 → 0.17.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/dist/branch.object.d.ts +9 -1
- package/dist/branch.object.d.ts.map +1 -1
- package/dist/branch.object.js +14 -1
- package/dist/company-branch.object.d.ts +5 -4
- package/dist/company-branch.object.d.ts.map +1 -1
- package/dist/company-branch.object.js +6 -4
- package/dist/company-department.object.d.ts +6 -6
- package/dist/company-department.object.d.ts.map +1 -1
- package/dist/company-department.object.js +7 -6
- package/dist/department.object.d.ts +9 -1
- package/dist/department.object.d.ts.map +1 -1
- package/dist/department.object.js +14 -1
- package/dist/features/access/access-admin.access.d.ts.map +1 -1
- package/dist/features/access/access-admin.access.js +8 -1
- package/dist/features/access/index.d.ts +1 -0
- package/dist/features/access/index.d.ts.map +1 -1
- package/dist/features/access/index.js +10 -2
- package/dist/features/access/org-structure.access.d.ts +3 -0
- package/dist/features/access/org-structure.access.d.ts.map +1 -0
- package/dist/features/access/org-structure.access.js +41 -0
- package/dist/features/access/user-management.access.d.ts.map +1 -1
- package/dist/features/access/user-management.access.js +5 -0
- package/dist/features/audit/audit-read.service.d.ts +32 -1
- package/dist/features/audit/audit-read.service.d.ts.map +1 -1
- package/dist/features/audit/audit-read.service.js +67 -14
- package/dist/features/audit/auth-audit-sink.d.ts +19 -0
- package/dist/features/audit/auth-audit-sink.d.ts.map +1 -0
- package/dist/features/audit/auth-audit-sink.js +24 -0
- package/dist/features/audit/directory-label-resolver.d.ts +10 -1
- package/dist/features/audit/directory-label-resolver.d.ts.map +1 -1
- package/dist/features/audit/directory-label-resolver.js +28 -1
- package/dist/features/authz/grant-admin.controller.d.ts +4 -4
- package/dist/features/authz/grant-admin.controller.js +4 -4
- package/dist/features/authz/grant-admin.service.d.ts +9 -8
- package/dist/features/authz/grant-admin.service.d.ts.map +1 -1
- package/dist/features/authz/grant-admin.service.js +20 -24
- package/dist/features/authz/grant-ceiling.d.ts +35 -16
- package/dist/features/authz/grant-ceiling.d.ts.map +1 -1
- package/dist/features/authz/grant-ceiling.js +44 -29
- package/dist/features/authz/role-binding.d.ts +25 -24
- package/dist/features/authz/role-binding.d.ts.map +1 -1
- package/dist/features/authz/role-binding.js +31 -44
- package/dist/features/identity-feature.module.d.ts +0 -11
- package/dist/features/identity-feature.module.d.ts.map +1 -1
- package/dist/features/identity-feature.module.js +90 -1
- package/dist/features/membership/membership.controller.d.ts.map +1 -1
- package/dist/features/membership/membership.controller.js +20 -6
- package/dist/features/membership/membership.service.d.ts +16 -10
- package/dist/features/membership/membership.service.d.ts.map +1 -1
- package/dist/features/membership/membership.service.js +61 -21
- package/dist/features/membership/posting.service.d.ts +25 -6
- package/dist/features/membership/posting.service.d.ts.map +1 -1
- package/dist/features/membership/posting.service.js +63 -16
- package/dist/features/org-bootstrap/org-bootstrap-file.d.ts +6 -2
- package/dist/features/org-bootstrap/org-bootstrap-file.d.ts.map +1 -1
- package/dist/features/org-bootstrap/org-bootstrap-file.js +54 -11
- package/dist/features/org-bootstrap/org-bootstrap-plan.d.ts +30 -11
- package/dist/features/org-bootstrap/org-bootstrap-plan.d.ts.map +1 -1
- package/dist/features/org-bootstrap/org-bootstrap-plan.js +172 -68
- package/dist/features/org-bootstrap/org-state.reader.d.ts.map +1 -1
- package/dist/features/org-bootstrap/org-state.reader.js +56 -49
- package/dist/features/org-bootstrap/run-org-bootstrap.d.ts +6 -1
- package/dist/features/org-bootstrap/run-org-bootstrap.d.ts.map +1 -1
- package/dist/features/org-bootstrap/run-org-bootstrap.js +81 -92
- package/dist/features/org-structure/org-catalog.controller.d.ts +29 -6
- package/dist/features/org-structure/org-catalog.controller.d.ts.map +1 -1
- package/dist/features/org-structure/org-catalog.controller.js +105 -17
- package/dist/features/org-structure/org-catalog.service.d.ts +99 -18
- package/dist/features/org-structure/org-catalog.service.d.ts.map +1 -1
- package/dist/features/org-structure/org-catalog.service.js +327 -51
- package/dist/features/org-structure/org-unit-name.d.ts +31 -0
- package/dist/features/org-structure/org-unit-name.d.ts.map +1 -0
- package/dist/features/org-structure/org-unit-name.js +50 -0
- package/dist/features/role/role.controller.d.ts +28 -13
- package/dist/features/role/role.controller.d.ts.map +1 -1
- package/dist/features/role/role.controller.js +54 -14
- package/dist/features/role/role.service.d.ts +76 -21
- package/dist/features/role/role.service.d.ts.map +1 -1
- package/dist/features/role/role.service.js +204 -27
- package/dist/features/security-policy/security-policy-version-bump.d.ts +41 -0
- package/dist/features/security-policy/security-policy-version-bump.d.ts.map +1 -0
- package/dist/features/security-policy/security-policy-version-bump.js +65 -0
- package/dist/features/security-policy/security-policy.controller.d.ts +51 -0
- package/dist/features/security-policy/security-policy.controller.d.ts.map +1 -0
- package/dist/features/security-policy/security-policy.controller.js +121 -0
- package/dist/features/security-policy/security-policy.service.d.ts +94 -0
- package/dist/features/security-policy/security-policy.service.d.ts.map +1 -0
- package/dist/features/security-policy/security-policy.service.js +429 -0
- package/dist/features/service-account/service-account.service.d.ts +13 -3
- package/dist/features/service-account/service-account.service.d.ts.map +1 -1
- package/dist/features/service-account/service-account.service.js +48 -10
- package/dist/features/service-account/service-account.store.d.ts +11 -1
- package/dist/features/service-account/service-account.store.d.ts.map +1 -1
- package/dist/features/service-account/service-account.store.js +20 -1
- package/dist/features/user/signin-admin.tokens.d.ts +11 -0
- package/dist/features/user/signin-admin.tokens.d.ts.map +1 -0
- package/dist/features/user/signin-admin.tokens.js +13 -0
- package/dist/features/user/user-mfa-status.d.ts +38 -0
- package/dist/features/user/user-mfa-status.d.ts.map +1 -0
- package/dist/features/user/user-mfa-status.js +47 -0
- package/dist/features/user/user-signin-admin.service.d.ts +48 -0
- package/dist/features/user/user-signin-admin.service.d.ts.map +1 -0
- package/dist/features/user/user-signin-admin.service.js +232 -0
- package/dist/features/user/user.controller.d.ts +29 -2
- package/dist/features/user/user.controller.d.ts.map +1 -1
- package/dist/features/user/user.controller.js +71 -3
- package/dist/features/user/user.service.d.ts +40 -14
- package/dist/features/user/user.service.d.ts.map +1 -1
- package/dist/features/user/user.service.js +143 -10
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/objects.d.ts.map +1 -1
- package/dist/objects.js +2 -0
- package/dist/role.object.d.ts +1 -1
- package/dist/role.object.d.ts.map +1 -1
- package/dist/role.object.js +6 -15
- package/dist/security-policy.object.d.ts +43 -0
- package/dist/security-policy.object.d.ts.map +1 -0
- package/dist/security-policy.object.js +107 -0
- package/dist/user.object.d.ts +6 -0
- package/dist/user.object.d.ts.map +1 -1
- package/dist/user.object.js +13 -1
- package/migrations/1710000000103-AddUserLoginName.cjs +87 -0
- package/migrations/1710000000104-CreateSecurityPolicy.cjs +90 -0
- package/migrations/1710000000105-OwnDepartmentsAndBranches.cjs +327 -0
- package/migrations/1710000000106-OwnRolesPerCompany.cjs +622 -0
- package/package.json +4 -4
|
@@ -1,32 +1,113 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* OrgCatalogService — company-
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* OrgCatalogService — a company's own departments and branches (G-012, spec §5.A), through the
|
|
3
|
+
* RecordsService facade (the engine pipe: gate, wall, validate, write, audit).
|
|
4
|
+
*
|
|
5
|
+
* Ownership (R1): every department/branch row belongs to exactly one company (`companyUuid`,
|
|
6
|
+
* immutable), stamped here from the reached company, never from a request body; its one junction
|
|
7
|
+
* row (`company_department` / `company_branch`) is written in the same transaction (R4).
|
|
8
|
+
*
|
|
9
|
+
* Wall (R2): every route reaches the company or the row through the engine's subtree `readScope`
|
|
10
|
+
* first; out of reach ⇒ {@link IdentityNotFoundError} (404, never a 403 that reveals existence).
|
|
11
|
+
*
|
|
12
|
+
* Races (R3): create/rename/delete lock first (`lockForUpdate`), then re-read through the wall and
|
|
13
|
+
* check under the lock; the per-company unique index is the backstop (a driver 1062 ⇒ the same 409).
|
|
14
|
+
*
|
|
15
|
+
* The one raw statement (R7) is the delete's fixed, parameterised, read-only in-use `COUNT(*)` on
|
|
16
|
+
* the engine transaction's own connection: complete (no record policy can narrow it), after the row
|
|
17
|
+
* lock, so a concurrent posting add (which locks the same row, `PostingService`) is either counted
|
|
18
|
+
* or refused.
|
|
6
19
|
*/
|
|
7
20
|
import { RecordsService } from '@astram/engine';
|
|
8
|
-
import {
|
|
21
|
+
import type { FrameworkSchemas } from '@astram/core';
|
|
22
|
+
import { type ListQueryDto } from '../shared/list-query.js';
|
|
23
|
+
/** Most departments (and, separately, branches) one company may hold (spec §10). */
|
|
24
|
+
export declare const ORG_UNIT_CAP = 1000;
|
|
25
|
+
/** One department/branch option: `uuid` is the option value, `name` the label (never a uuid). */
|
|
26
|
+
export interface OrgUnitOption {
|
|
27
|
+
uuid: string;
|
|
28
|
+
name: string;
|
|
29
|
+
/** Postings the caller can see that reference this row; omitted when it cannot read postings. */
|
|
30
|
+
usedBy?: number;
|
|
31
|
+
}
|
|
32
|
+
/** A company's departments or branches: one page, or every row (up to the cap). */
|
|
33
|
+
export interface OrgUnitList {
|
|
34
|
+
rows: OrgUnitOption[];
|
|
35
|
+
total: number;
|
|
36
|
+
page: number;
|
|
37
|
+
limit: number;
|
|
38
|
+
}
|
|
9
39
|
export declare class OrgCatalogService {
|
|
10
40
|
private readonly records;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
41
|
+
private readonly schemas?;
|
|
42
|
+
constructor(records: RecordsService, schemas?: (FrameworkSchemas | null) | undefined);
|
|
43
|
+
/** A company's own departments (404 when the company is out of reach). */
|
|
44
|
+
listCompanyDepartments(companyUuid: string, query?: ListQueryDto): Promise<OrgUnitList>;
|
|
45
|
+
/** A company's own branches (404 when the company is out of reach). */
|
|
46
|
+
listCompanyBranches(companyUuid: string, query?: ListQueryDto): Promise<OrgUnitList>;
|
|
47
|
+
createDepartment(companyUuid: string, name: unknown): Promise<{
|
|
48
|
+
uuid: string;
|
|
49
|
+
name: string;
|
|
50
|
+
}>;
|
|
51
|
+
createBranch(companyUuid: string, name: unknown): Promise<{
|
|
52
|
+
uuid: string;
|
|
53
|
+
name: string;
|
|
54
|
+
}>;
|
|
55
|
+
renameDepartment(uuid: string, name: unknown): Promise<void>;
|
|
56
|
+
renameBranch(uuid: string, name: unknown): Promise<void>;
|
|
57
|
+
deleteDepartment(uuid: string): Promise<void>;
|
|
58
|
+
deleteBranch(uuid: string): Promise<void>;
|
|
16
59
|
/**
|
|
17
|
-
* The id of
|
|
18
|
-
*
|
|
60
|
+
* The id of `companyUuid`'s department named `name` (normalised, spec §10), creating it with its
|
|
61
|
+
* owner and junction row when absent (org bootstrap, G-009 R5). A found row gets its junction
|
|
62
|
+
* row back if it lacks one (idempotent re-runs). Reads no company (the bootstrap's org-units step
|
|
63
|
+
* holds no company key), so the company is not walled here: the engine's create wall is.
|
|
19
64
|
*/
|
|
20
|
-
ensureDepartment(name: string): Promise<string>;
|
|
21
|
-
/** The id of
|
|
22
|
-
ensureBranch(name: string): Promise<string>;
|
|
65
|
+
ensureDepartment(companyUuid: string, name: string): Promise<string>;
|
|
66
|
+
/** The id of `companyUuid`'s branch named `name`, creating it when absent. */
|
|
67
|
+
ensureBranch(companyUuid: string, name: string): Promise<string>;
|
|
23
68
|
/** Link a department to a company (the junction row); idempotent. */
|
|
24
69
|
attachDepartment(companyUuid: string, departmentUuid: string): Promise<void>;
|
|
25
70
|
/** Link a branch to a company (the junction row); idempotent. */
|
|
26
71
|
attachBranch(companyUuid: string, branchUuid: string): Promise<void>;
|
|
27
|
-
private
|
|
72
|
+
private validName;
|
|
73
|
+
private listUnits;
|
|
74
|
+
/**
|
|
75
|
+
* `usedBy` (NAES review #3/A): ONE engine aggregate over `membership_posting`, grouped by the
|
|
76
|
+
* unit, over exactly the returned ids. Gated as a posting read: a caller who cannot read postings
|
|
77
|
+
* gets the rows without `usedBy` (only `AuthorizationDeniedError` is caught); any other failure
|
|
78
|
+
* (`EngineUnavailableError` ⇒ 503) fails the request, never silently drops the counts.
|
|
79
|
+
*/
|
|
80
|
+
private withUsedBy;
|
|
81
|
+
private createUnit;
|
|
82
|
+
private renameUnit;
|
|
83
|
+
private deleteUnit;
|
|
84
|
+
/**
|
|
85
|
+
* Lock the row FIRST, then re-read it through the wall (404 when out of reach or gone). A legacy
|
|
86
|
+
* row with no owner is root-only (the wall already hides it from a non-root; checked again here).
|
|
87
|
+
*/
|
|
88
|
+
private lockAndReach;
|
|
89
|
+
/** Duplicate check, cap, then the row and its junction row, on the caller's transaction. */
|
|
90
|
+
private insertUnit;
|
|
91
|
+
/**
|
|
92
|
+
* Refuse a name another row of the same company already holds. The engine's equality runs under
|
|
93
|
+
* the column collation (`utf8mb4_unicode_ci`: case, accents and width fold), so "Qualite" finds
|
|
94
|
+
* "Qualité"; the 409 names the STORED clashing row.
|
|
95
|
+
*/
|
|
96
|
+
private assertNameFree;
|
|
97
|
+
private findByName;
|
|
98
|
+
private duplicate;
|
|
99
|
+
/**
|
|
100
|
+
* Run a create/rename; a driver duplicate key (the per-company unique index, the backstop for a
|
|
101
|
+
* concurrent write) becomes the same 409 naming the stored row. Errno only: the driver's message
|
|
102
|
+
* holds the value and is never echoed (KI #87).
|
|
103
|
+
*/
|
|
104
|
+
private mappingDuplicates;
|
|
105
|
+
/**
|
|
106
|
+
* The complete in-use count (spec §5.A, NAES third pass): one fixed framework statement on the
|
|
107
|
+
* engine transaction's own connection, after the row lock. Any failure ⇒ 503, nothing deleted.
|
|
108
|
+
*/
|
|
109
|
+
private countPostings;
|
|
110
|
+
private ensureUnit;
|
|
28
111
|
private attachJunction;
|
|
29
|
-
/** Shared reader for a company-scoped catalog junction; lists then name-resolves. */
|
|
30
|
-
private listCompanyCatalog;
|
|
31
112
|
}
|
|
32
113
|
//# sourceMappingURL=org-catalog.service.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"org-catalog.service.d.ts","sourceRoot":"","sources":["../../../src/features/org-structure/org-catalog.service.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"org-catalog.service.d.ts","sourceRoot":"","sources":["../../../src/features/org-structure/org-catalog.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,OAAO,EAEL,cAAc,EAMf,MAAM,gBAAgB,CAAC;AAExB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAS5E,oFAAoF;AACpF,eAAO,MAAM,YAAY,OAAO,CAAC;AAEjC,iGAAiG;AACjG,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,iGAAiG;IACjG,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,mFAAmF;AACnF,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,aAAa,EAAE,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AA8DD,qBACa,iBAAiB;IAE1B,OAAO,CAAC,QAAQ,CAAC,OAAO;IAKxB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;gBALR,OAAO,EAAE,cAAc,EAKvB,OAAO,CAAC,GAAE,gBAAgB,GAAG,IAAI,aAAA;IAKpD,0EAA0E;IAC1E,sBAAsB,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC;IAIvF,uEAAuE;IACvE,mBAAmB,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC;IAMpF,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAI7F,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAIzF,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAI5D,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAIxD,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI7C,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAMzC;;;;;OAKG;IACH,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAIpE,8EAA8E;IAC9E,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAIhE,qEAAqE;IACrE,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI5E,iEAAiE;IACjE,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAMpE,OAAO,CAAC,SAAS;YAMH,SAAS;IAsCvB;;;;;OAKG;YACW,UAAU;YAwBV,UAAU;YAyBV,UAAU;YAkBV,UAAU;IAWxB;;;OAGG;YACW,YAAY;IAW1B,4FAA4F;YAC9E,UAAU;IAoBxB;;;;OAIG;YACW,cAAc;YAWd,UAAU;IAaxB,OAAO,CAAC,SAAS;IAMjB;;;;OAIG;YACW,iBAAiB;IAqB/B;;;OAGG;YACW,aAAa;YAiBb,UAAU;YAgBV,cAAc;CAc7B"}
|
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* OrgCatalogService — company-
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* OrgCatalogService — a company's own departments and branches (G-012, spec §5.A), through the
|
|
4
|
+
* RecordsService facade (the engine pipe: gate, wall, validate, write, audit).
|
|
5
|
+
*
|
|
6
|
+
* Ownership (R1): every department/branch row belongs to exactly one company (`companyUuid`,
|
|
7
|
+
* immutable), stamped here from the reached company, never from a request body; its one junction
|
|
8
|
+
* row (`company_department` / `company_branch`) is written in the same transaction (R4).
|
|
9
|
+
*
|
|
10
|
+
* Wall (R2): every route reaches the company or the row through the engine's subtree `readScope`
|
|
11
|
+
* first; out of reach ⇒ {@link IdentityNotFoundError} (404, never a 403 that reveals existence).
|
|
12
|
+
*
|
|
13
|
+
* Races (R3): create/rename/delete lock first (`lockForUpdate`), then re-read through the wall and
|
|
14
|
+
* check under the lock; the per-company unique index is the backstop (a driver 1062 ⇒ the same 409).
|
|
15
|
+
*
|
|
16
|
+
* The one raw statement (R7) is the delete's fixed, parameterised, read-only in-use `COUNT(*)` on
|
|
17
|
+
* the engine transaction's own connection: complete (no record policy can narrow it), after the row
|
|
18
|
+
* lock, so a concurrent posting add (which locks the same row, `PostingService`) is either counted
|
|
19
|
+
* or refused.
|
|
7
20
|
*/
|
|
8
21
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
9
22
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
@@ -14,83 +27,346 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
14
27
|
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
15
28
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
16
29
|
};
|
|
30
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
31
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
32
|
+
};
|
|
17
33
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
-
exports.OrgCatalogService = void 0;
|
|
34
|
+
exports.OrgCatalogService = exports.ORG_UNIT_CAP = void 0;
|
|
19
35
|
const common_1 = require("@nestjs/common");
|
|
20
36
|
const engine_1 = require("@astram/engine");
|
|
37
|
+
const internal_1 = require("@astram/engine/internal");
|
|
38
|
+
const framework_schemas_token_js_1 = require("../framework-schemas.token.js");
|
|
39
|
+
const list_query_js_1 = require("../shared/list-query.js");
|
|
40
|
+
const identity_errors_js_1 = require("../shared/identity-errors.js");
|
|
41
|
+
const org_unit_name_js_1 = require("./org-unit-name.js");
|
|
42
|
+
/** Most departments (and, separately, branches) one company may hold (spec §10). */
|
|
43
|
+
exports.ORG_UNIT_CAP = 1000;
|
|
44
|
+
const DEPARTMENT = {
|
|
45
|
+
kind: 'department',
|
|
46
|
+
typeKey: 'department',
|
|
47
|
+
junctionTypeKey: 'company_department',
|
|
48
|
+
refFieldKey: 'departmentUuid',
|
|
49
|
+
postingColumn: 'mp_department_uuid',
|
|
50
|
+
plural: 'departments',
|
|
51
|
+
};
|
|
52
|
+
const BRANCH = {
|
|
53
|
+
kind: 'branch',
|
|
54
|
+
typeKey: 'branch',
|
|
55
|
+
junctionTypeKey: 'company_branch',
|
|
56
|
+
refFieldKey: 'branchUuid',
|
|
57
|
+
postingColumn: 'mp_branch_uuid',
|
|
58
|
+
plural: 'branches',
|
|
59
|
+
};
|
|
60
|
+
const IN_USE = 'Still used by one or more assignments; move them first.';
|
|
61
|
+
const COUNT_UNAVAILABLE = 'Could not check whether it is still in use; nothing was deleted. Try again.';
|
|
21
62
|
/** Department/branch rows have a single uuid key; a composite id here is a wiring defect. */
|
|
22
63
|
function catalogId(id) {
|
|
23
64
|
if (typeof id !== 'string')
|
|
24
65
|
throw new TypeError('catalog row id is not a single uuid');
|
|
25
66
|
return id;
|
|
26
67
|
}
|
|
68
|
+
/** A driver duplicate-key error, by errno/code only (its message names the value: never echoed). */
|
|
69
|
+
function isDuplicateKey(err) {
|
|
70
|
+
if (typeof err !== 'object' || err === null)
|
|
71
|
+
return false;
|
|
72
|
+
const e = err;
|
|
73
|
+
return e.errno === 1062 || e.code === 'ER_DUP_ENTRY';
|
|
74
|
+
}
|
|
75
|
+
const present = (v) => typeof v === 'string' && v.trim().length > 0;
|
|
76
|
+
function nameOf(view) {
|
|
77
|
+
const name = view.fields?.name;
|
|
78
|
+
return typeof name === 'string' && name.length > 0 ? name : '(unknown)';
|
|
79
|
+
}
|
|
80
|
+
function ownerOf(view) {
|
|
81
|
+
const owner = view.fields?.companyUuid;
|
|
82
|
+
return typeof owner === 'string' && owner.length > 0 ? owner : null;
|
|
83
|
+
}
|
|
27
84
|
let OrgCatalogService = class OrgCatalogService {
|
|
28
85
|
records;
|
|
29
|
-
|
|
86
|
+
schemas;
|
|
87
|
+
constructor(records, schemas) {
|
|
30
88
|
this.records = records;
|
|
89
|
+
this.schemas = schemas;
|
|
90
|
+
}
|
|
91
|
+
// ------------------------------------------------------------------------------- lists
|
|
92
|
+
/** A company's own departments (404 when the company is out of reach). */
|
|
93
|
+
listCompanyDepartments(companyUuid, query) {
|
|
94
|
+
return this.listUnits(DEPARTMENT, companyUuid, query);
|
|
31
95
|
}
|
|
32
|
-
/** A company's
|
|
33
|
-
|
|
34
|
-
return this.
|
|
96
|
+
/** A company's own branches (404 when the company is out of reach). */
|
|
97
|
+
listCompanyBranches(companyUuid, query) {
|
|
98
|
+
return this.listUnits(BRANCH, companyUuid, query);
|
|
35
99
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return this.
|
|
100
|
+
// ------------------------------------------------------------------------------- writes
|
|
101
|
+
createDepartment(companyUuid, name) {
|
|
102
|
+
return this.createUnit(DEPARTMENT, companyUuid, name);
|
|
39
103
|
}
|
|
104
|
+
createBranch(companyUuid, name) {
|
|
105
|
+
return this.createUnit(BRANCH, companyUuid, name);
|
|
106
|
+
}
|
|
107
|
+
renameDepartment(uuid, name) {
|
|
108
|
+
return this.renameUnit(DEPARTMENT, uuid, name);
|
|
109
|
+
}
|
|
110
|
+
renameBranch(uuid, name) {
|
|
111
|
+
return this.renameUnit(BRANCH, uuid, name);
|
|
112
|
+
}
|
|
113
|
+
deleteDepartment(uuid) {
|
|
114
|
+
return this.deleteUnit(DEPARTMENT, uuid);
|
|
115
|
+
}
|
|
116
|
+
deleteBranch(uuid) {
|
|
117
|
+
return this.deleteUnit(BRANCH, uuid);
|
|
118
|
+
}
|
|
119
|
+
// ------------------------------------------------------------------------------- bootstrap
|
|
40
120
|
/**
|
|
41
|
-
* The id of
|
|
42
|
-
*
|
|
121
|
+
* The id of `companyUuid`'s department named `name` (normalised, spec §10), creating it with its
|
|
122
|
+
* owner and junction row when absent (org bootstrap, G-009 R5). A found row gets its junction
|
|
123
|
+
* row back if it lacks one (idempotent re-runs). Reads no company (the bootstrap's org-units step
|
|
124
|
+
* holds no company key), so the company is not walled here: the engine's create wall is.
|
|
43
125
|
*/
|
|
44
|
-
ensureDepartment(name) {
|
|
45
|
-
return this.
|
|
126
|
+
ensureDepartment(companyUuid, name) {
|
|
127
|
+
return this.ensureUnit(DEPARTMENT, companyUuid, name);
|
|
46
128
|
}
|
|
47
|
-
/** The id of
|
|
48
|
-
ensureBranch(name) {
|
|
49
|
-
return this.
|
|
129
|
+
/** The id of `companyUuid`'s branch named `name`, creating it when absent. */
|
|
130
|
+
ensureBranch(companyUuid, name) {
|
|
131
|
+
return this.ensureUnit(BRANCH, companyUuid, name);
|
|
50
132
|
}
|
|
51
133
|
/** Link a department to a company (the junction row); idempotent. */
|
|
52
134
|
attachDepartment(companyUuid, departmentUuid) {
|
|
53
|
-
return this.attachJunction(
|
|
135
|
+
return this.attachJunction(DEPARTMENT, companyUuid, departmentUuid);
|
|
54
136
|
}
|
|
55
137
|
/** Link a branch to a company (the junction row); idempotent. */
|
|
56
138
|
attachBranch(companyUuid, branchUuid) {
|
|
57
|
-
return this.attachJunction(
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
return
|
|
65
|
-
}
|
|
66
|
-
async
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
139
|
+
return this.attachJunction(BRANCH, companyUuid, branchUuid);
|
|
140
|
+
}
|
|
141
|
+
// ------------------------------------------------------------------------------- internals
|
|
142
|
+
validName(spec, raw) {
|
|
143
|
+
const result = (0, org_unit_name_js_1.normaliseOrgUnitName)(raw);
|
|
144
|
+
if (!result.ok)
|
|
145
|
+
throw new identity_errors_js_1.IdentityBadRequestError((0, org_unit_name_js_1.orgUnitNameMessage)(spec.kind, result.reason));
|
|
146
|
+
return result.value;
|
|
147
|
+
}
|
|
148
|
+
async listUnits(spec, companyUuid, query) {
|
|
149
|
+
// Reach the company through the subtree wall first (NAES review B): 404 when out of reach.
|
|
150
|
+
const company = await this.records.get('company', companyUuid);
|
|
151
|
+
if (company === null)
|
|
152
|
+
throw new identity_errors_js_1.IdentityNotFoundError('Company not found.');
|
|
153
|
+
const owner = catalogId(company.id);
|
|
154
|
+
const paged = present(query?.page) || present(query?.limit) || present(query?.search);
|
|
155
|
+
let page = 1;
|
|
156
|
+
let limit = exports.ORG_UNIT_CAP;
|
|
157
|
+
const listQuery = {
|
|
158
|
+
where: { companyUuid: owner },
|
|
159
|
+
sortBy: 'name',
|
|
160
|
+
sortOrder: 'asc',
|
|
161
|
+
limit: exports.ORG_UNIT_CAP,
|
|
162
|
+
offset: 0,
|
|
163
|
+
};
|
|
164
|
+
if (paged) {
|
|
165
|
+
const parsed = (0, list_query_js_1.parseListQuery)(query);
|
|
166
|
+
page = parsed.page;
|
|
167
|
+
limit = parsed.limit;
|
|
168
|
+
listQuery.limit = parsed.limit;
|
|
169
|
+
listQuery.offset = parsed.params.offset ?? 0;
|
|
170
|
+
if (parsed.params.search !== undefined) {
|
|
171
|
+
listQuery.search = { fields: ['name'], term: parsed.params.search };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const result = await this.records.list(spec.typeKey, listQuery);
|
|
175
|
+
const rows = result.rows.map((r) => ({
|
|
176
|
+
uuid: catalogId(r.id),
|
|
177
|
+
name: nameOf(r),
|
|
178
|
+
}));
|
|
179
|
+
return { rows: await this.withUsedBy(spec, rows), total: result.total, page, limit };
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* `usedBy` (NAES review #3/A): ONE engine aggregate over `membership_posting`, grouped by the
|
|
183
|
+
* unit, over exactly the returned ids. Gated as a posting read: a caller who cannot read postings
|
|
184
|
+
* gets the rows without `usedBy` (only `AuthorizationDeniedError` is caught); any other failure
|
|
185
|
+
* (`EngineUnavailableError` ⇒ 503) fails the request, never silently drops the counts.
|
|
186
|
+
*/
|
|
187
|
+
async withUsedBy(spec, rows) {
|
|
188
|
+
if (rows.length === 0)
|
|
189
|
+
return rows;
|
|
190
|
+
let groups;
|
|
191
|
+
try {
|
|
192
|
+
groups = (await this.records.aggregate('membership_posting', {
|
|
193
|
+
where: { [spec.refFieldKey]: { in: rows.map((r) => r.uuid) } },
|
|
194
|
+
groupBy: [spec.refFieldKey],
|
|
195
|
+
measures: [{ fn: 'count', as: 'used_by' }],
|
|
196
|
+
limit: rows.length,
|
|
197
|
+
})).groups;
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
if (err instanceof engine_1.AuthorizationDeniedError)
|
|
201
|
+
return rows;
|
|
202
|
+
throw err;
|
|
203
|
+
}
|
|
204
|
+
const counts = new Map();
|
|
205
|
+
for (const g of groups) {
|
|
206
|
+
const key = g.key[spec.refFieldKey];
|
|
207
|
+
if (typeof key === 'string')
|
|
208
|
+
counts.set(key.toLowerCase(), Number(g.values.used_by ?? 0));
|
|
209
|
+
}
|
|
210
|
+
return rows.map((r) => ({ ...r, usedBy: counts.get(r.uuid.toLowerCase()) ?? 0 }));
|
|
211
|
+
}
|
|
212
|
+
async createUnit(spec, companyUuid, rawName) {
|
|
213
|
+
const name = this.validName(spec, rawName);
|
|
214
|
+
let owner = companyUuid;
|
|
215
|
+
const uuid = await this.mappingDuplicates(spec, () => owner, name, () => this.records.transaction(async (tx) => {
|
|
216
|
+
// Lock the company FIRST (the snapshot of the reads below is taken after it), then reach
|
|
217
|
+
// it through the wall: out of reach or gone ⇒ 404.
|
|
218
|
+
await tx.lockForUpdate('company', companyUuid);
|
|
219
|
+
const company = await tx.get('company', companyUuid);
|
|
220
|
+
if (company === null)
|
|
221
|
+
throw new identity_errors_js_1.IdentityNotFoundError('Company not found.');
|
|
222
|
+
owner = catalogId(company.id);
|
|
223
|
+
return this.insertUnit(tx, spec, owner, name);
|
|
224
|
+
}));
|
|
225
|
+
return { uuid, name };
|
|
226
|
+
}
|
|
227
|
+
async renameUnit(spec, uuid, rawName) {
|
|
228
|
+
const name = this.validName(spec, rawName);
|
|
229
|
+
let owner = null;
|
|
230
|
+
await this.mappingDuplicates(spec, () => owner, name, () => this.records.transaction(async (tx) => {
|
|
231
|
+
const row = await this.lockAndReach(tx, spec, uuid);
|
|
232
|
+
owner = ownerOf(row);
|
|
233
|
+
if (row.fields?.name === name)
|
|
234
|
+
return; // same name: no write
|
|
235
|
+
await this.assertNameFree(tx, spec, owner, name, catalogId(row.id));
|
|
236
|
+
await tx.update(spec.typeKey, row.id, { fields: { name } });
|
|
237
|
+
}));
|
|
238
|
+
}
|
|
239
|
+
async deleteUnit(spec, uuid) {
|
|
240
|
+
await this.records.transaction(async (tx) => {
|
|
241
|
+
const row = await this.lockAndReach(tx, spec, uuid);
|
|
242
|
+
// R5: complete count under the row lock; any posting (visible or not) ⇒ 409, no number.
|
|
243
|
+
if ((await this.countPostings(tx, spec, catalogId(row.id))) > 0) {
|
|
244
|
+
throw new identity_errors_js_1.IdentityConflictError(IN_USE);
|
|
245
|
+
}
|
|
246
|
+
await tx.delete(spec.typeKey, row.id); // the junction row cascades
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Lock the row FIRST, then re-read it through the wall (404 when out of reach or gone). A legacy
|
|
251
|
+
* row with no owner is root-only (the wall already hides it from a non-root; checked again here).
|
|
252
|
+
*/
|
|
253
|
+
async lockAndReach(tx, spec, uuid) {
|
|
254
|
+
const label = spec.kind === 'department' ? 'Department' : 'Branch';
|
|
255
|
+
await tx.lockForUpdate(spec.typeKey, uuid);
|
|
256
|
+
const row = await tx.get(spec.typeKey, uuid);
|
|
257
|
+
if (row === null)
|
|
258
|
+
throw new identity_errors_js_1.IdentityNotFoundError(`${label} not found.`);
|
|
259
|
+
if (ownerOf(row) === null && this.records.requestPrincipal().isRoot !== true) {
|
|
260
|
+
throw new identity_errors_js_1.IdentityNotFoundError(`${label} not found.`);
|
|
261
|
+
}
|
|
262
|
+
return row;
|
|
263
|
+
}
|
|
264
|
+
/** Duplicate check, cap, then the row and its junction row, on the caller's transaction. */
|
|
265
|
+
async insertUnit(tx, spec, owner, name) {
|
|
266
|
+
await this.assertNameFree(tx, spec, owner, name, undefined);
|
|
267
|
+
const held = (await tx.list(spec.typeKey, { where: { companyUuid: owner }, limit: 1 })).total;
|
|
268
|
+
if (held >= exports.ORG_UNIT_CAP) {
|
|
269
|
+
throw new identity_errors_js_1.IdentityConflictError(`This company already has the maximum of ${exports.ORG_UNIT_CAP} ${spec.plural}.`);
|
|
270
|
+
}
|
|
271
|
+
const id = catalogId(await tx.create(spec.typeKey, { fields: { name, companyUuid: owner } }));
|
|
272
|
+
await tx.create(spec.junctionTypeKey, {
|
|
273
|
+
fields: { companyUuid: owner, [spec.refFieldKey]: id },
|
|
72
274
|
});
|
|
275
|
+
return id;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Refuse a name another row of the same company already holds. The engine's equality runs under
|
|
279
|
+
* the column collation (`utf8mb4_unicode_ci`: case, accents and width fold), so "Qualite" finds
|
|
280
|
+
* "Qualité"; the 409 names the STORED clashing row.
|
|
281
|
+
*/
|
|
282
|
+
async assertNameFree(tx, spec, owner, name, self) {
|
|
283
|
+
const clash = await this.findByName(tx, spec, owner, name, self);
|
|
284
|
+
if (clash !== undefined)
|
|
285
|
+
throw this.duplicate(spec, nameOf(clash));
|
|
286
|
+
}
|
|
287
|
+
async findByName(reader, spec, owner, name, self) {
|
|
288
|
+
const rows = (await reader.list(spec.typeKey, { where: { companyUuid: owner, name }, limit: 2 })).rows;
|
|
289
|
+
return rows.find((r) => self === undefined || catalogId(r.id) !== self);
|
|
290
|
+
}
|
|
291
|
+
duplicate(spec, storedName) {
|
|
292
|
+
return new identity_errors_js_1.IdentityConflictError(`A ${spec.kind} named "${storedName}" already exists in this company.`);
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Run a create/rename; a driver duplicate key (the per-company unique index, the backstop for a
|
|
296
|
+
* concurrent write) becomes the same 409 naming the stored row. Errno only: the driver's message
|
|
297
|
+
* holds the value and is never echoed (KI #87).
|
|
298
|
+
*/
|
|
299
|
+
async mappingDuplicates(spec, owner, name, write) {
|
|
300
|
+
try {
|
|
301
|
+
return await write();
|
|
302
|
+
}
|
|
303
|
+
catch (err) {
|
|
304
|
+
if (!isDuplicateKey(err))
|
|
305
|
+
throw err;
|
|
306
|
+
let stored = name;
|
|
307
|
+
try {
|
|
308
|
+
const clash = await this.findByName(this.records, spec, owner(), name);
|
|
309
|
+
if (clash !== undefined)
|
|
310
|
+
stored = nameOf(clash);
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
// The 409 stands; the submitted name names the clash.
|
|
314
|
+
}
|
|
315
|
+
throw this.duplicate(spec, stored);
|
|
316
|
+
}
|
|
73
317
|
}
|
|
74
|
-
/**
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
318
|
+
/**
|
|
319
|
+
* The complete in-use count (spec §5.A, NAES third pass): one fixed framework statement on the
|
|
320
|
+
* engine transaction's own connection, after the row lock. Any failure ⇒ 503, nothing deleted.
|
|
321
|
+
*/
|
|
322
|
+
async countPostings(tx, spec, id) {
|
|
323
|
+
const schemas = this.schemas;
|
|
324
|
+
if (!schemas)
|
|
325
|
+
throw new identity_errors_js_1.IdentityUnavailableError(COUNT_UNAVAILABLE);
|
|
326
|
+
let n;
|
|
327
|
+
try {
|
|
328
|
+
const rows = (await (0, internal_1.transactionQueryOf)(tx)(`SELECT COUNT(*) AS n FROM \`${schemas.app}\`.\`membership_posting\` WHERE ${spec.postingColumn} = ?`, [id]));
|
|
329
|
+
n = Number(rows[0]?.n);
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
332
|
+
throw new identity_errors_js_1.IdentityUnavailableError(COUNT_UNAVAILABLE);
|
|
88
333
|
}
|
|
89
|
-
|
|
334
|
+
if (!Number.isFinite(n))
|
|
335
|
+
throw new identity_errors_js_1.IdentityUnavailableError(COUNT_UNAVAILABLE);
|
|
336
|
+
return n;
|
|
337
|
+
}
|
|
338
|
+
async ensureUnit(spec, companyUuid, rawName) {
|
|
339
|
+
const name = this.validName(spec, rawName);
|
|
340
|
+
const found = await this.findByName(this.records, spec, companyUuid, name);
|
|
341
|
+
if (found !== undefined) {
|
|
342
|
+
const id = catalogId(found.id);
|
|
343
|
+
await this.attachJunction(spec, companyUuid, id);
|
|
344
|
+
return id;
|
|
345
|
+
}
|
|
346
|
+
return this.records.transaction(async (tx) => {
|
|
347
|
+
await tx.lockForUpdate('company', companyUuid);
|
|
348
|
+
const now = await this.findByName(tx, spec, companyUuid, name);
|
|
349
|
+
if (now !== undefined)
|
|
350
|
+
return catalogId(now.id);
|
|
351
|
+
return this.insertUnit(tx, spec, companyUuid, name);
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
async attachJunction(spec, companyUuid, refUuid) {
|
|
355
|
+
const page = await this.records.list(spec.junctionTypeKey, {
|
|
356
|
+
where: { companyUuid, [spec.refFieldKey]: refUuid },
|
|
357
|
+
limit: 1,
|
|
358
|
+
});
|
|
359
|
+
if (page.rows.length > 0)
|
|
360
|
+
return;
|
|
361
|
+
await this.records.create(spec.junctionTypeKey, {
|
|
362
|
+
fields: { companyUuid, [spec.refFieldKey]: refUuid },
|
|
363
|
+
});
|
|
90
364
|
}
|
|
91
365
|
};
|
|
92
366
|
exports.OrgCatalogService = OrgCatalogService;
|
|
93
367
|
exports.OrgCatalogService = OrgCatalogService = __decorate([
|
|
94
368
|
(0, common_1.Injectable)(),
|
|
95
|
-
|
|
369
|
+
__param(1, (0, common_1.Optional)()),
|
|
370
|
+
__param(1, (0, common_1.Inject)(framework_schemas_token_js_1.IDENTITY_FRAMEWORK_SCHEMAS)),
|
|
371
|
+
__metadata("design:paramtypes", [engine_1.RecordsService, Object])
|
|
96
372
|
], OrgCatalogService);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* G-012 (spec §10): the ONE name rule for departments and branches, shared by the create and rename
|
|
3
|
+
* routes, `OrgCatalogService.ensure*` and the org bootstrap planner, so "Quality Assurance" in a
|
|
4
|
+
* bootstrap file and "Quality Assurance" typed in the UI are the same name.
|
|
5
|
+
*
|
|
6
|
+
* Order (spec §10, as written): Unicode NFC; trim and collapse every whitespace run (JS `\s`, so
|
|
7
|
+
* tab, CR and LF too) to one space; refuse any control character left (U+0000–U+001F,
|
|
8
|
+
* U+007F–U+009F, e.g. NUL or NEL); refuse an empty result or one longer than
|
|
9
|
+
* {@link ORG_UNIT_NAME_MAX} characters (code points, the column width). The template's
|
|
10
|
+
* `validateName` (`pages/org-structure/orgStructure.js`) applies the same order, so the client
|
|
11
|
+
* never accepts a name the server refuses.
|
|
12
|
+
*
|
|
13
|
+
* Case, accents and width are NOT folded here: the stored name keeps what the user typed; the
|
|
14
|
+
* column collation (`utf8mb4_unicode_ci`) decides uniqueness, and the bootstrap planner compares
|
|
15
|
+
* with `nameKey` on top of this.
|
|
16
|
+
*/
|
|
17
|
+
/** The column width of `d_name` / `b_name` (VARCHAR(150), counted in characters). */
|
|
18
|
+
export declare const ORG_UNIT_NAME_MAX = 150;
|
|
19
|
+
export type OrgUnitNameRefusal = 'not_text' | 'empty' | 'control_character' | 'too_long';
|
|
20
|
+
export type OrgUnitNameResult = {
|
|
21
|
+
readonly ok: true;
|
|
22
|
+
readonly value: string;
|
|
23
|
+
} | {
|
|
24
|
+
readonly ok: false;
|
|
25
|
+
readonly reason: OrgUnitNameRefusal;
|
|
26
|
+
};
|
|
27
|
+
/** Normalise a department or branch name, or say why it is refused. Pure. */
|
|
28
|
+
export declare function normaliseOrgUnitName(raw: unknown): OrgUnitNameResult;
|
|
29
|
+
/** The fixed, user-facing 400 text for a refusal (`kind` is "department" or "branch"). */
|
|
30
|
+
export declare function orgUnitNameMessage(kind: 'department' | 'branch', reason: OrgUnitNameRefusal): string;
|
|
31
|
+
//# sourceMappingURL=org-unit-name.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"org-unit-name.d.ts","sourceRoot":"","sources":["../../../src/features/org-structure/org-unit-name.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,qFAAqF;AACrF,eAAO,MAAM,iBAAiB,MAAM,CAAC;AAErC,MAAM,MAAM,kBAAkB,GAAG,UAAU,GAAG,OAAO,GAAG,mBAAmB,GAAG,UAAU,CAAC;AAEzF,MAAM,MAAM,iBAAiB,GACzB;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC7C;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAA;CAAE,CAAC;AAKhE,6EAA6E;AAC7E,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,GAAG,iBAAiB,CAQpE;AAED,0FAA0F;AAC1F,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,YAAY,GAAG,QAAQ,EAC7B,MAAM,EAAE,kBAAkB,GACzB,MAAM,CASR"}
|