@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
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework identity migration — each company OWNS its departments and branches (G-012, spec §4).
|
|
3
|
+
*
|
|
4
|
+
* WHY: `departments` / `branches` were global catalogs (one "Sales" row shared by every company
|
|
5
|
+
* that used the name, `UNIQUE (d_name)`), so no company could safely rename or delete its own.
|
|
6
|
+
* After this migration every row has one owner company (`d_company_uuid` / `b_company_uuid`),
|
|
7
|
+
* names are unique per company, and no row is shared between companies.
|
|
8
|
+
*
|
|
9
|
+
* Steps, per table (idempotent; a half-applied state is completed, a re-run changes nothing):
|
|
10
|
+
* 1. ADD the nullable owner column, its index `idx_<table>_company` and
|
|
11
|
+
* `FK_<table>_company` -> `companies (c_uuid)` ON DELETE CASCADE, each when absent.
|
|
12
|
+
* 2. OWNER. A row with no owner that a junction row links gets the linked company with the
|
|
13
|
+
* smallest `c_uuid`. A row with no owner and no junction row that postings reference gets the
|
|
14
|
+
* posting's membership company with the smallest `c_uuid`. Every owned row then gets a
|
|
15
|
+
* junction row to its owner when it has none, so no posting points at an unowned row. Only
|
|
16
|
+
* `IS NULL` owners are written: a re-run never overwrites an owner.
|
|
17
|
+
* 3. DROP the global unique `IDX_<table>_name` when present (copies share the name).
|
|
18
|
+
* 4. SPLIT. For every (company, row) where the company is not the row's owner — a junction row,
|
|
19
|
+
* or a posting of that company's memberships — the company gets its own copy: an existing
|
|
20
|
+
* row it owns with the same name (column collation) is reused, else one is inserted
|
|
21
|
+
* (`UUID()`, same name, that owner). The company's junction row to the copy is added, that
|
|
22
|
+
* company's postings are repointed to the copy, and only then its junction row to the
|
|
23
|
+
* original is deleted (creates before deletes, so a re-run after a partial failure resumes).
|
|
24
|
+
* A re-run finds no (company, row) off the owner and does nothing.
|
|
25
|
+
* 5. ADD UNIQUE `IDX_<table>_company_name (owner, name)` when absent; duplicates (only possible
|
|
26
|
+
* on a database that never had the global unique) fail with the names, nothing indexed.
|
|
27
|
+
* A row linked to no company and referenced by no posting keeps a NULL owner (root only).
|
|
28
|
+
*
|
|
29
|
+
* `down`: refuses (no change) while any name exists more than once in a table whose global
|
|
30
|
+
* unique is absent; otherwise drops the per-company unique, re-adds the global unique, drops the
|
|
31
|
+
* FK, the index and the column (IF EXISTS each). Split copies are not merged back.
|
|
32
|
+
*
|
|
33
|
+
* Names: schema from `ctx.schemas` (`frameworkSchemasForMigration`), so the four-database,
|
|
34
|
+
* namespace and single layouts all work (single: framework tables unprefixed in one database, as
|
|
35
|
+
* in `...101`). Literal table/column/index names, every value `?`-bound, no hardcoded ids. Sorts
|
|
36
|
+
* after `...001` (catalogs), `...002` (junctions, postings) and `...102` (the latest published).
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
const { frameworkSchemasForMigration } = require('@astram/core');
|
|
40
|
+
|
|
41
|
+
const MIGRATION = '1710000000105-OwnDepartmentsAndBranches';
|
|
42
|
+
/** Duplicate groups listed in a failure message; the rest are counted. */
|
|
43
|
+
const MAX_LISTED = 50;
|
|
44
|
+
|
|
45
|
+
const KINDS = [
|
|
46
|
+
{
|
|
47
|
+
label: 'department',
|
|
48
|
+
table: 'departments',
|
|
49
|
+
pk: 'd_uuid',
|
|
50
|
+
name: 'd_name',
|
|
51
|
+
created: 'd_created_at',
|
|
52
|
+
owner: 'd_company_uuid',
|
|
53
|
+
index: 'idx_departments_company',
|
|
54
|
+
fk: 'FK_departments_company',
|
|
55
|
+
globalUnique: 'IDX_departments_name',
|
|
56
|
+
companyUnique: 'IDX_departments_company_name',
|
|
57
|
+
junction: 'company_departments',
|
|
58
|
+
jCompany: 'cd_company_uuid',
|
|
59
|
+
jUnit: 'cd_department_uuid',
|
|
60
|
+
posting: 'mp_department_uuid',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
label: 'branch',
|
|
64
|
+
table: 'branches',
|
|
65
|
+
pk: 'b_uuid',
|
|
66
|
+
name: 'b_name',
|
|
67
|
+
created: 'b_created_at',
|
|
68
|
+
owner: 'b_company_uuid',
|
|
69
|
+
index: 'idx_branches_company',
|
|
70
|
+
fk: 'FK_branches_company',
|
|
71
|
+
globalUnique: 'IDX_branches_name',
|
|
72
|
+
companyUnique: 'IDX_branches_company_name',
|
|
73
|
+
junction: 'company_branches',
|
|
74
|
+
jCompany: 'cb_company_uuid',
|
|
75
|
+
jUnit: 'cb_branch_uuid',
|
|
76
|
+
posting: 'mp_branch_uuid',
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
const rowsOf = (result) => (Array.isArray(result) ? result : []);
|
|
81
|
+
|
|
82
|
+
const present = async (query, sql, params) => rowsOf(await query(sql, params)).length > 0;
|
|
83
|
+
|
|
84
|
+
const hasColumn = (query, APP, table, column) =>
|
|
85
|
+
present(
|
|
86
|
+
query,
|
|
87
|
+
`SELECT 1 AS n FROM information_schema.COLUMNS
|
|
88
|
+
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME = ? LIMIT 1`,
|
|
89
|
+
[APP, table, column],
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const hasIndex = (query, APP, table, index) =>
|
|
93
|
+
present(
|
|
94
|
+
query,
|
|
95
|
+
`SELECT 1 AS n FROM information_schema.STATISTICS
|
|
96
|
+
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND INDEX_NAME = ? LIMIT 1`,
|
|
97
|
+
[APP, table, index],
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const hasFk = (query, APP, table, fk) =>
|
|
101
|
+
present(
|
|
102
|
+
query,
|
|
103
|
+
`SELECT 1 AS n FROM information_schema.REFERENTIAL_CONSTRAINTS
|
|
104
|
+
WHERE CONSTRAINT_SCHEMA = ? AND TABLE_NAME = ? AND CONSTRAINT_NAME = ? LIMIT 1`,
|
|
105
|
+
[APP, table, fk],
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
/** Step 1: owner column, its index and FK, each when absent. */
|
|
109
|
+
async function addOwnerColumn(query, APP, k) {
|
|
110
|
+
const T = `\`${APP}\`.\`${k.table}\``;
|
|
111
|
+
if (!(await hasColumn(query, APP, k.table, k.owner))) {
|
|
112
|
+
await query(`ALTER TABLE ${T} ADD COLUMN ${k.owner} VARCHAR(36) NULL`);
|
|
113
|
+
}
|
|
114
|
+
if (!(await hasIndex(query, APP, k.table, k.index))) {
|
|
115
|
+
await query(`CREATE INDEX ${k.index} ON ${T} (${k.owner})`);
|
|
116
|
+
}
|
|
117
|
+
if (!(await hasFk(query, APP, k.table, k.fk))) {
|
|
118
|
+
await query(`ALTER TABLE ${T}
|
|
119
|
+
ADD CONSTRAINT ${k.fk} FOREIGN KEY (${k.owner})
|
|
120
|
+
REFERENCES \`${APP}\`.\`companies\` (c_uuid) ON DELETE CASCADE`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Step 2: owners from junctions, then from postings; every owned row linked to its owner. */
|
|
125
|
+
async function assignOwners(query, APP, k) {
|
|
126
|
+
const T = `\`${APP}\`.\`${k.table}\``;
|
|
127
|
+
const J = `\`${APP}\`.\`${k.junction}\``;
|
|
128
|
+
const MP = `\`${APP}\`.\`membership_posting\``;
|
|
129
|
+
const CM = `\`${APP}\`.\`company_membership\``;
|
|
130
|
+
|
|
131
|
+
await query(`UPDATE ${T} u
|
|
132
|
+
SET u.${k.owner} = (SELECT MIN(j.${k.jCompany}) FROM ${J} j WHERE j.${k.jUnit} = u.${k.pk})
|
|
133
|
+
WHERE u.${k.owner} IS NULL
|
|
134
|
+
AND EXISTS (SELECT 1 FROM ${J} j WHERE j.${k.jUnit} = u.${k.pk})`);
|
|
135
|
+
|
|
136
|
+
await query(`UPDATE ${T} u
|
|
137
|
+
SET u.${k.owner} = (SELECT MIN(cm.cm_company_uuid)
|
|
138
|
+
FROM ${MP} mp
|
|
139
|
+
JOIN ${CM} cm ON cm.cm_uuid = mp.mp_membership_uuid
|
|
140
|
+
WHERE mp.${k.posting} = u.${k.pk})
|
|
141
|
+
WHERE u.${k.owner} IS NULL
|
|
142
|
+
AND NOT EXISTS (SELECT 1 FROM ${J} j WHERE j.${k.jUnit} = u.${k.pk})
|
|
143
|
+
AND EXISTS (SELECT 1 FROM ${MP} mp WHERE mp.${k.posting} = u.${k.pk})`);
|
|
144
|
+
|
|
145
|
+
const unlinked = rowsOf(
|
|
146
|
+
await query(`SELECT u.${k.owner} AS company, u.${k.pk} AS unit FROM ${T} u
|
|
147
|
+
WHERE u.${k.owner} IS NOT NULL
|
|
148
|
+
AND NOT EXISTS (SELECT 1 FROM ${J} j
|
|
149
|
+
WHERE j.${k.jCompany} = u.${k.owner} AND j.${k.jUnit} = u.${k.pk})
|
|
150
|
+
ORDER BY company, unit`),
|
|
151
|
+
);
|
|
152
|
+
for (const { company, unit } of unlinked) await link(query, APP, k, company, unit);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Insert the junction row (company, unit) when absent. */
|
|
156
|
+
async function link(query, APP, k, company, unit) {
|
|
157
|
+
const J = `\`${APP}\`.\`${k.junction}\``;
|
|
158
|
+
await query(
|
|
159
|
+
`INSERT INTO ${J} (${k.jCompany}, ${k.jUnit})
|
|
160
|
+
SELECT ?, ? FROM DUAL
|
|
161
|
+
WHERE NOT EXISTS (SELECT 1 FROM ${J} WHERE ${k.jCompany} = ? AND ${k.jUnit} = ?)`,
|
|
162
|
+
[company, unit, company, unit],
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Step 4: give every non-owner company that uses a row its own copy. */
|
|
167
|
+
async function splitShared(query, APP, k) {
|
|
168
|
+
const T = `\`${APP}\`.\`${k.table}\``;
|
|
169
|
+
const J = `\`${APP}\`.\`${k.junction}\``;
|
|
170
|
+
const MP = `\`${APP}\`.\`membership_posting\``;
|
|
171
|
+
const CM = `\`${APP}\`.\`company_membership\``;
|
|
172
|
+
|
|
173
|
+
const pairs = rowsOf(
|
|
174
|
+
await query(`SELECT j.${k.jCompany} AS company, u.${k.pk} AS unit, u.${k.name} AS name
|
|
175
|
+
FROM ${J} j
|
|
176
|
+
JOIN ${T} u ON u.${k.pk} = j.${k.jUnit}
|
|
177
|
+
WHERE u.${k.owner} IS NOT NULL AND j.${k.jCompany} <> u.${k.owner}
|
|
178
|
+
UNION
|
|
179
|
+
SELECT cm.cm_company_uuid AS company, u.${k.pk} AS unit, u.${k.name} AS name
|
|
180
|
+
FROM ${MP} mp
|
|
181
|
+
JOIN ${CM} cm ON cm.cm_uuid = mp.mp_membership_uuid
|
|
182
|
+
JOIN ${T} u ON u.${k.pk} = mp.${k.posting}
|
|
183
|
+
WHERE u.${k.owner} IS NOT NULL AND cm.cm_company_uuid <> u.${k.owner}
|
|
184
|
+
ORDER BY company, unit`),
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const findOwned = async (company, name) => {
|
|
188
|
+
const rows = rowsOf(
|
|
189
|
+
await query(
|
|
190
|
+
`SELECT ${k.pk} AS uuid FROM ${T} WHERE ${k.owner} = ? AND ${k.name} = ? ORDER BY ${k.pk} LIMIT 1`,
|
|
191
|
+
[company, name],
|
|
192
|
+
),
|
|
193
|
+
);
|
|
194
|
+
return rows.length > 0 ? String(rows[0].uuid) : null;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
for (const { company, unit, name } of pairs) {
|
|
198
|
+
let copy = await findOwned(company, name);
|
|
199
|
+
if (copy === null) {
|
|
200
|
+
await query(
|
|
201
|
+
`INSERT INTO ${T} (${k.pk}, ${k.name}, ${k.owner}, ${k.created})
|
|
202
|
+
SELECT UUID(), src.${k.name}, ?, src.${k.created} FROM ${T} src WHERE src.${k.pk} = ?`,
|
|
203
|
+
[company, unit],
|
|
204
|
+
);
|
|
205
|
+
copy = await findOwned(company, name);
|
|
206
|
+
if (copy === null) {
|
|
207
|
+
throw new Error(`${MIGRATION}: could not create a company-owned ${k.label} copy; re-run.`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
await link(query, APP, k, company, copy);
|
|
211
|
+
// Resume safety (security review P2): after a partial failure a membership may already hold the
|
|
212
|
+
// same posting (role + the other org unit) on the copy. Repointing its twin would break
|
|
213
|
+
// `IDX_mp_unique` (1062) and fail every re-run, so the twin on the original is removed first;
|
|
214
|
+
// the posting on the copy keeps the assignment.
|
|
215
|
+
const other = k.posting === 'mp_department_uuid' ? 'mp_branch_uuid' : 'mp_department_uuid';
|
|
216
|
+
await query(
|
|
217
|
+
`DELETE mp FROM ${MP} mp
|
|
218
|
+
JOIN ${CM} cm ON cm.cm_uuid = mp.mp_membership_uuid
|
|
219
|
+
JOIN ${MP} twin ON twin.mp_membership_uuid = mp.mp_membership_uuid
|
|
220
|
+
AND twin.mp_role_uuid = mp.mp_role_uuid
|
|
221
|
+
AND twin.${other} <=> mp.${other}
|
|
222
|
+
AND twin.${k.posting} = ?
|
|
223
|
+
WHERE mp.${k.posting} = ? AND cm.cm_company_uuid = ?`,
|
|
224
|
+
[copy, unit, company],
|
|
225
|
+
);
|
|
226
|
+
await query(
|
|
227
|
+
`UPDATE ${MP} mp
|
|
228
|
+
JOIN ${CM} cm ON cm.cm_uuid = mp.mp_membership_uuid
|
|
229
|
+
SET mp.${k.posting} = ?
|
|
230
|
+
WHERE mp.${k.posting} = ? AND cm.cm_company_uuid = ?`,
|
|
231
|
+
[copy, unit, company],
|
|
232
|
+
);
|
|
233
|
+
await query(`DELETE FROM ${J} WHERE ${k.jCompany} = ? AND ${k.jUnit} = ?`, [company, unit]);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function duplicateError(k, APP, groups, scope, tail) {
|
|
238
|
+
const listed = groups.slice(0, MAX_LISTED).map((g) => ` - "${g.name}": ${g.n} rows`);
|
|
239
|
+
if (groups.length > MAX_LISTED) listed.push(` - ... and ${groups.length - MAX_LISTED} more`);
|
|
240
|
+
return new Error(
|
|
241
|
+
`${MIGRATION}: ${groups.length} ${k.label} name(s) occur more than once ${scope} in ` +
|
|
242
|
+
`${APP}.${k.table} (compared as the column collation does: case- and accent-insensitive):\n` +
|
|
243
|
+
`${listed.join('\n')}\n${tail}`,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Step 5: per-company unique when absent; duplicates fail with their names. */
|
|
248
|
+
async function addCompanyUnique(query, APP, k) {
|
|
249
|
+
if (await hasIndex(query, APP, k.table, k.companyUnique)) return;
|
|
250
|
+
const groups = rowsOf(
|
|
251
|
+
await query(`SELECT MIN(${k.name}) AS name, COUNT(*) AS n
|
|
252
|
+
FROM \`${APP}\`.\`${k.table}\`
|
|
253
|
+
WHERE ${k.owner} IS NOT NULL
|
|
254
|
+
GROUP BY ${k.owner}, ${k.name}
|
|
255
|
+
HAVING COUNT(*) > 1
|
|
256
|
+
ORDER BY n DESC`),
|
|
257
|
+
);
|
|
258
|
+
if (groups.length > 0) {
|
|
259
|
+
throw duplicateError(
|
|
260
|
+
k,
|
|
261
|
+
APP,
|
|
262
|
+
groups,
|
|
263
|
+
'within one company',
|
|
264
|
+
`No unique index was created. Rename or remove the duplicates, then re-run astram-migrate.`,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
await query(
|
|
268
|
+
`CREATE UNIQUE INDEX ${k.companyUnique} ON \`${APP}\`.\`${k.table}\` (${k.owner}, ${k.name})`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function up(ctx) {
|
|
273
|
+
const { query } = ctx;
|
|
274
|
+
const APP = frameworkSchemasForMigration(ctx, process.env).app;
|
|
275
|
+
for (const k of KINDS) {
|
|
276
|
+
await addOwnerColumn(query, APP, k);
|
|
277
|
+
await assignOwners(query, APP, k);
|
|
278
|
+
if (await hasIndex(query, APP, k.table, k.globalUnique)) {
|
|
279
|
+
await query(`DROP INDEX ${k.globalUnique} ON \`${APP}\`.\`${k.table}\``);
|
|
280
|
+
}
|
|
281
|
+
await splitShared(query, APP, k);
|
|
282
|
+
await addCompanyUnique(query, APP, k);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function down(ctx) {
|
|
287
|
+
const { query } = ctx;
|
|
288
|
+
const APP = frameworkSchemasForMigration(ctx, process.env).app;
|
|
289
|
+
|
|
290
|
+
// Pre-check every table before changing any: the global unique cannot come back while a name
|
|
291
|
+
// is held by more than one row (a split copy, or two companies' own rows).
|
|
292
|
+
const restore = [];
|
|
293
|
+
for (const k of KINDS) {
|
|
294
|
+
if (await hasIndex(query, APP, k.table, k.globalUnique)) continue;
|
|
295
|
+
const groups = rowsOf(
|
|
296
|
+
await query(`SELECT MIN(${k.name}) AS name, COUNT(*) AS n
|
|
297
|
+
FROM \`${APP}\`.\`${k.table}\`
|
|
298
|
+
GROUP BY ${k.name}
|
|
299
|
+
HAVING COUNT(*) > 1
|
|
300
|
+
ORDER BY n DESC`),
|
|
301
|
+
);
|
|
302
|
+
if (groups.length > 0) {
|
|
303
|
+
throw duplicateError(
|
|
304
|
+
k,
|
|
305
|
+
APP,
|
|
306
|
+
groups,
|
|
307
|
+
'across companies',
|
|
308
|
+
'Nothing was rolled back: the global unique name index cannot be restored while these ' +
|
|
309
|
+
'exist (split copies are not merged back). Rename or remove them first.',
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
restore.push(k);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
for (const k of KINDS) {
|
|
316
|
+
const T = `\`${APP}\`.\`${k.table}\``;
|
|
317
|
+
await query(`DROP INDEX IF EXISTS ${k.companyUnique} ON ${T}`);
|
|
318
|
+
if (restore.includes(k)) {
|
|
319
|
+
await query(`CREATE UNIQUE INDEX IF NOT EXISTS ${k.globalUnique} ON ${T} (${k.name})`);
|
|
320
|
+
}
|
|
321
|
+
await query(`ALTER TABLE ${T} DROP FOREIGN KEY IF EXISTS ${k.fk}`);
|
|
322
|
+
await query(`DROP INDEX IF EXISTS ${k.index} ON ${T}`);
|
|
323
|
+
await query(`ALTER TABLE ${T} DROP COLUMN IF EXISTS ${k.owner}`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
module.exports = { up, down };
|