@objectstack/plugin-org-scoping 7.0.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/LICENSE +93 -0
- package/README.md +55 -0
- package/dist/index.d.mts +190 -0
- package/dist/index.d.ts +190 -0
- package/dist/index.js +626 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +592 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +55 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
ORG_SCOPING_PLUGIN_ID: () => ORG_SCOPING_PLUGIN_ID,
|
|
24
|
+
ORG_SCOPING_PLUGIN_VERSION: () => ORG_SCOPING_PLUGIN_VERSION,
|
|
25
|
+
OrgScopingPlugin: () => OrgScopingPlugin,
|
|
26
|
+
claimOrphanOrgRows: () => claimOrphanOrgRows,
|
|
27
|
+
cloneOrgSeedData: () => cloneOrgSeedData,
|
|
28
|
+
ensureDefaultOrganization: () => ensureDefaultOrganization,
|
|
29
|
+
orgScopingObjects: () => orgScopingObjects,
|
|
30
|
+
orgScopingPluginManifestHeader: () => orgScopingPluginManifestHeader
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(index_exports);
|
|
33
|
+
|
|
34
|
+
// src/claim-orphan-org-rows.ts
|
|
35
|
+
var SYSTEM_CTX = { isSystem: true };
|
|
36
|
+
function hasOrganizationField(schema) {
|
|
37
|
+
const fields = schema?.fields;
|
|
38
|
+
if (!fields) return false;
|
|
39
|
+
if (Array.isArray(fields)) {
|
|
40
|
+
return fields.some((f) => f?.name === "organization_id");
|
|
41
|
+
}
|
|
42
|
+
return Object.prototype.hasOwnProperty.call(fields, "organization_id");
|
|
43
|
+
}
|
|
44
|
+
async function claimOrphanOrgRows(ql, organizationId, options = {}) {
|
|
45
|
+
const logger = options.logger;
|
|
46
|
+
if (!ql || typeof ql.update !== "function" || typeof ql.find !== "function") {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
const registry = ql.registry;
|
|
50
|
+
if (!registry || typeof registry.getAllObjects !== "function") {
|
|
51
|
+
logger?.warn?.("[org-scoping] claimOrphanOrgRows: registry unavailable");
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
const schemas = registry.getAllObjects();
|
|
55
|
+
const results = [];
|
|
56
|
+
for (const schema of schemas) {
|
|
57
|
+
if (!schema?.name) continue;
|
|
58
|
+
if (schema.managedBy) continue;
|
|
59
|
+
if (schema.name.startsWith("sys_")) continue;
|
|
60
|
+
if (!hasOrganizationField(schema)) continue;
|
|
61
|
+
try {
|
|
62
|
+
const orphans = await ql.find(
|
|
63
|
+
schema.name,
|
|
64
|
+
{ where: { organization_id: null }, limit: 1e4, fields: ["id"] },
|
|
65
|
+
{ context: SYSTEM_CTX }
|
|
66
|
+
);
|
|
67
|
+
const list = Array.isArray(orphans) ? orphans : Array.isArray(orphans?.records) ? orphans.records : [];
|
|
68
|
+
if (list.length === 0) continue;
|
|
69
|
+
let updated = 0;
|
|
70
|
+
for (const row of list) {
|
|
71
|
+
if (!row?.id) continue;
|
|
72
|
+
try {
|
|
73
|
+
await ql.update(
|
|
74
|
+
schema.name,
|
|
75
|
+
{ id: row.id, organization_id: organizationId },
|
|
76
|
+
{ context: SYSTEM_CTX }
|
|
77
|
+
);
|
|
78
|
+
updated += 1;
|
|
79
|
+
} catch (e) {
|
|
80
|
+
logger?.warn?.(`[org-scoping] claim failed for ${schema.name}:${row.id}`, {
|
|
81
|
+
error: e.message
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (updated > 0) {
|
|
86
|
+
results.push({ object: schema.name, count: updated });
|
|
87
|
+
}
|
|
88
|
+
} catch (e) {
|
|
89
|
+
logger?.warn?.(`[org-scoping] claim scan failed for ${schema.name}`, {
|
|
90
|
+
error: e.message
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (results.length > 0) {
|
|
95
|
+
const total = results.reduce((s, r) => s + r.count, 0);
|
|
96
|
+
logger?.info?.(`[org-scoping] claimed ${total} orphan seed row(s) for organization ${organizationId}`, {
|
|
97
|
+
breakdown: results
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return results;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/clone-org-seed-data.ts
|
|
104
|
+
var SYSTEM_CTX2 = { isSystem: true };
|
|
105
|
+
var SKIP_COPY_FIELDS = /* @__PURE__ */ new Set([
|
|
106
|
+
"id",
|
|
107
|
+
"created_at",
|
|
108
|
+
"updated_at",
|
|
109
|
+
"organization_id"
|
|
110
|
+
]);
|
|
111
|
+
var SKIP_COPY_TYPES = /* @__PURE__ */ new Set(["formula", "summary"]);
|
|
112
|
+
function fieldList(schema) {
|
|
113
|
+
const fields = schema?.fields;
|
|
114
|
+
if (!fields) return [];
|
|
115
|
+
if (Array.isArray(fields)) {
|
|
116
|
+
return fields.map((f) => ({
|
|
117
|
+
name: f?.name,
|
|
118
|
+
type: f?.type,
|
|
119
|
+
reference: f?.reference,
|
|
120
|
+
multiple: f?.multiple,
|
|
121
|
+
unique: f?.unique
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
return Object.entries(fields).map(([name, f]) => ({
|
|
125
|
+
name,
|
|
126
|
+
type: f?.type,
|
|
127
|
+
reference: f?.reference,
|
|
128
|
+
multiple: f?.multiple,
|
|
129
|
+
unique: f?.unique
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
132
|
+
function isLookupField(f) {
|
|
133
|
+
return (f.type === "lookup" || f.type === "master_detail" || f.type === "tree") && !!f.reference;
|
|
134
|
+
}
|
|
135
|
+
function hasOrgField(schema) {
|
|
136
|
+
return fieldList(schema).some((f) => f.name === "organization_id");
|
|
137
|
+
}
|
|
138
|
+
function shortId() {
|
|
139
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-";
|
|
140
|
+
let out = "";
|
|
141
|
+
for (let i = 0; i < 16; i++) {
|
|
142
|
+
out += alphabet[Math.floor(Math.random() * alphabet.length)];
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
async function findDonorOrgId(ql) {
|
|
147
|
+
try {
|
|
148
|
+
const res = await ql.find(
|
|
149
|
+
"sys_organization",
|
|
150
|
+
{ orderBy: { created_at: "asc" }, limit: 1, fields: ["id"] },
|
|
151
|
+
{ context: SYSTEM_CTX2 }
|
|
152
|
+
);
|
|
153
|
+
const list = Array.isArray(res) ? res : Array.isArray(res?.records) ? res.records : [];
|
|
154
|
+
return list[0]?.id ?? null;
|
|
155
|
+
} catch {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async function cloneOrgSeedData(ql, targetOrgId, options = {}) {
|
|
160
|
+
const logger = options.logger;
|
|
161
|
+
if (!ql || typeof ql.find !== "function" || typeof ql.insert !== "function") {
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
const registry = ql.registry;
|
|
165
|
+
if (!registry || typeof registry.getAllObjects !== "function") {
|
|
166
|
+
logger?.warn?.("[org-scoping] cloneOrgSeedData: registry unavailable");
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
const donorOrgId = await findDonorOrgId(ql);
|
|
170
|
+
if (!donorOrgId) return [];
|
|
171
|
+
if (donorOrgId === targetOrgId) return [];
|
|
172
|
+
const schemas = registry.getAllObjects().filter(
|
|
173
|
+
(s) => s?.name && !s.managedBy && !s.name.startsWith("sys_") && hasOrgField(s)
|
|
174
|
+
);
|
|
175
|
+
const remap = {};
|
|
176
|
+
const summary = [];
|
|
177
|
+
const inserted = [];
|
|
178
|
+
for (const schema of schemas) {
|
|
179
|
+
const objectName = schema.name;
|
|
180
|
+
try {
|
|
181
|
+
const existing = await ql.find(
|
|
182
|
+
objectName,
|
|
183
|
+
{ where: { organization_id: targetOrgId }, limit: 1, fields: ["id"] },
|
|
184
|
+
{ context: SYSTEM_CTX2 }
|
|
185
|
+
);
|
|
186
|
+
const existingList = Array.isArray(existing) ? existing : Array.isArray(existing?.records) ? existing.records : [];
|
|
187
|
+
if (existingList.length > 0) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const donorRows = await ql.find(
|
|
191
|
+
objectName,
|
|
192
|
+
{ where: { organization_id: donorOrgId }, limit: 1e4 },
|
|
193
|
+
{ context: SYSTEM_CTX2 }
|
|
194
|
+
);
|
|
195
|
+
const rows = Array.isArray(donorRows) ? donorRows : Array.isArray(donorRows?.records) ? donorRows.records : [];
|
|
196
|
+
if (rows.length === 0) continue;
|
|
197
|
+
const fields = fieldList(schema);
|
|
198
|
+
const lookups = fields.filter(isLookupField);
|
|
199
|
+
const uniqueFields = fields.filter((f) => f.unique && !SKIP_COPY_FIELDS.has(f.name));
|
|
200
|
+
const objectRemap = remap[objectName] ?? (remap[objectName] = {});
|
|
201
|
+
let cloned = 0;
|
|
202
|
+
for (const row of rows) {
|
|
203
|
+
const newId = shortId();
|
|
204
|
+
const data = { id: newId, organization_id: targetOrgId };
|
|
205
|
+
for (const f of fields) {
|
|
206
|
+
if (SKIP_COPY_FIELDS.has(f.name)) continue;
|
|
207
|
+
if (f.type && SKIP_COPY_TYPES.has(f.type)) continue;
|
|
208
|
+
if (row[f.name] === void 0) continue;
|
|
209
|
+
data[f.name] = row[f.name];
|
|
210
|
+
}
|
|
211
|
+
const suffix = `+${targetOrgId.slice(-6)}`;
|
|
212
|
+
for (const uf of uniqueFields) {
|
|
213
|
+
const v = data[uf.name];
|
|
214
|
+
if (typeof v !== "string" || !v) continue;
|
|
215
|
+
if (uf.type === "email" && v.includes("@")) {
|
|
216
|
+
const [local, domain] = v.split("@");
|
|
217
|
+
data[uf.name] = `clone-${targetOrgId.slice(-6)}-${local}@${domain}`;
|
|
218
|
+
} else {
|
|
219
|
+
data[uf.name] = `${v}${suffix}`;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
await ql.insert(objectName, data, { context: SYSTEM_CTX2 });
|
|
224
|
+
objectRemap[row.id] = newId;
|
|
225
|
+
inserted.push({ object: objectName, newId, record: data, lookups });
|
|
226
|
+
cloned++;
|
|
227
|
+
} catch (e) {
|
|
228
|
+
logger?.warn?.("[org-scoping] cloneOrgSeedData: insert failed", {
|
|
229
|
+
object: objectName,
|
|
230
|
+
error: e.message
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (cloned > 0) summary.push({ object: objectName, count: cloned });
|
|
235
|
+
} catch (e) {
|
|
236
|
+
logger?.warn?.("[org-scoping] cloneOrgSeedData: object failed", {
|
|
237
|
+
object: objectName,
|
|
238
|
+
error: e.message
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
for (const item of inserted) {
|
|
243
|
+
if (item.lookups.length === 0) continue;
|
|
244
|
+
const patch = {};
|
|
245
|
+
let dirty = false;
|
|
246
|
+
for (const f of item.lookups) {
|
|
247
|
+
const oldVal = item.record[f.name];
|
|
248
|
+
if (oldVal == null) continue;
|
|
249
|
+
const targetMap = remap[f.reference];
|
|
250
|
+
if (Array.isArray(oldVal)) {
|
|
251
|
+
const next = oldVal.map((v) => typeof v === "string" && targetMap?.[v] || null).filter((v) => v != null);
|
|
252
|
+
if (next.length !== oldVal.length || next.some((v, i) => v !== oldVal[i])) {
|
|
253
|
+
patch[f.name] = next.length > 0 ? next : null;
|
|
254
|
+
dirty = true;
|
|
255
|
+
}
|
|
256
|
+
} else if (typeof oldVal === "string") {
|
|
257
|
+
if (targetMap && targetMap[oldVal]) {
|
|
258
|
+
patch[f.name] = targetMap[oldVal];
|
|
259
|
+
dirty = true;
|
|
260
|
+
} else {
|
|
261
|
+
patch[f.name] = null;
|
|
262
|
+
dirty = true;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (!dirty) continue;
|
|
267
|
+
try {
|
|
268
|
+
await ql.update(item.object, { id: item.newId, ...patch }, { context: SYSTEM_CTX2 });
|
|
269
|
+
} catch (e) {
|
|
270
|
+
logger?.warn?.("[org-scoping] cloneOrgSeedData: lookup remap failed", {
|
|
271
|
+
object: item.object,
|
|
272
|
+
id: item.newId,
|
|
273
|
+
error: e.message
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return summary;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// src/ensure-default-organization.ts
|
|
281
|
+
var SYSTEM_CTX3 = { isSystem: true };
|
|
282
|
+
async function tryFind(ql, object, where, limit = 100) {
|
|
283
|
+
try {
|
|
284
|
+
const rows = await ql.find(object, { where, limit }, { context: SYSTEM_CTX3 });
|
|
285
|
+
return Array.isArray(rows) ? rows : Array.isArray(rows?.records) ? rows.records : [];
|
|
286
|
+
} catch {
|
|
287
|
+
return [];
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
async function tryInsert(ql, object, data) {
|
|
291
|
+
try {
|
|
292
|
+
return await ql.insert(object, data, { context: SYSTEM_CTX3 });
|
|
293
|
+
} catch {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function genId(prefix) {
|
|
298
|
+
const rand = Math.random().toString(36).slice(2, 10);
|
|
299
|
+
const ts = Date.now().toString(36);
|
|
300
|
+
return `${prefix}_${ts}${rand}`;
|
|
301
|
+
}
|
|
302
|
+
async function ensureDefaultOrganization(ql, options = {}) {
|
|
303
|
+
const logger = options.logger;
|
|
304
|
+
if (!ql || typeof ql.find !== "function" || typeof ql.insert !== "function") {
|
|
305
|
+
return { defaultOrgCreated: false, memberCreated: false, reason: "no_admin" };
|
|
306
|
+
}
|
|
307
|
+
const adminPs = await tryFind(ql, "sys_permission_set", { name: "admin_full_access" }, 1);
|
|
308
|
+
if (adminPs.length === 0 || !adminPs[0].id) {
|
|
309
|
+
return { defaultOrgCreated: false, memberCreated: false, reason: "no_admin" };
|
|
310
|
+
}
|
|
311
|
+
const adminPsId = adminPs[0].id;
|
|
312
|
+
const adminGrants = await tryFind(
|
|
313
|
+
ql,
|
|
314
|
+
"sys_user_permission_set",
|
|
315
|
+
{ permission_set_id: adminPsId, organization_id: null },
|
|
316
|
+
50
|
|
317
|
+
);
|
|
318
|
+
if (adminGrants.length === 0) {
|
|
319
|
+
return { defaultOrgCreated: false, memberCreated: false, reason: "no_admin" };
|
|
320
|
+
}
|
|
321
|
+
const sortedGrants = [...adminGrants].sort((a, b) => {
|
|
322
|
+
const ta = a.created_at ? new Date(a.created_at).getTime() : 0;
|
|
323
|
+
const tb = b.created_at ? new Date(b.created_at).getTime() : 0;
|
|
324
|
+
return ta - tb;
|
|
325
|
+
});
|
|
326
|
+
const adminUserId = sortedGrants[0]?.user_id;
|
|
327
|
+
if (!adminUserId) {
|
|
328
|
+
return { defaultOrgCreated: false, memberCreated: false, reason: "no_admin" };
|
|
329
|
+
}
|
|
330
|
+
const memberships = await tryFind(ql, "sys_member", { user_id: adminUserId }, 1);
|
|
331
|
+
if (memberships.length > 0) {
|
|
332
|
+
return {
|
|
333
|
+
defaultOrgCreated: false,
|
|
334
|
+
memberCreated: false,
|
|
335
|
+
reason: "admin_already_in_org"
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
let defaultOrgId;
|
|
339
|
+
let defaultOrgCreated = false;
|
|
340
|
+
const existingDefault = await tryFind(ql, "sys_organization", { slug: "default" }, 1);
|
|
341
|
+
if (existingDefault.length > 0 && existingDefault[0].id) {
|
|
342
|
+
defaultOrgId = String(existingDefault[0].id);
|
|
343
|
+
} else {
|
|
344
|
+
const newOrgId = genId("org");
|
|
345
|
+
const orgRow = await tryInsert(ql, "sys_organization", {
|
|
346
|
+
id: newOrgId,
|
|
347
|
+
name: "Default Organization",
|
|
348
|
+
slug: "default",
|
|
349
|
+
logo: null,
|
|
350
|
+
metadata: null
|
|
351
|
+
});
|
|
352
|
+
if (!orgRow) {
|
|
353
|
+
logger?.warn?.("[org-scoping] failed to create default organization for platform admin");
|
|
354
|
+
return { defaultOrgCreated: false, memberCreated: false, reason: "org_insert_failed" };
|
|
355
|
+
}
|
|
356
|
+
defaultOrgId = orgRow?.id ?? newOrgId;
|
|
357
|
+
defaultOrgCreated = true;
|
|
358
|
+
}
|
|
359
|
+
const memRow = await tryInsert(ql, "sys_member", {
|
|
360
|
+
id: genId("mem"),
|
|
361
|
+
organization_id: defaultOrgId,
|
|
362
|
+
user_id: adminUserId,
|
|
363
|
+
role: "owner"
|
|
364
|
+
});
|
|
365
|
+
if (!memRow) {
|
|
366
|
+
logger?.warn?.("[org-scoping] failed to bind platform admin to default organization");
|
|
367
|
+
return {
|
|
368
|
+
defaultOrgCreated,
|
|
369
|
+
defaultOrgId,
|
|
370
|
+
memberCreated: false,
|
|
371
|
+
reason: "member_insert_failed"
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
logger?.info?.(
|
|
375
|
+
`[org-scoping] bound platform admin to default organization (${defaultOrgId})`,
|
|
376
|
+
{ userId: adminUserId, defaultOrgId }
|
|
377
|
+
);
|
|
378
|
+
return { defaultOrgCreated, defaultOrgId, memberCreated: true };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// src/manifest.ts
|
|
382
|
+
var ORG_SCOPING_PLUGIN_ID = "com.objectstack.plugin-org-scoping";
|
|
383
|
+
var ORG_SCOPING_PLUGIN_VERSION = "1.0.0";
|
|
384
|
+
var orgScopingObjects = [];
|
|
385
|
+
var orgScopingPluginManifestHeader = {
|
|
386
|
+
id: ORG_SCOPING_PLUGIN_ID,
|
|
387
|
+
namespace: "sys",
|
|
388
|
+
version: ORG_SCOPING_PLUGIN_VERSION,
|
|
389
|
+
type: "plugin",
|
|
390
|
+
scope: "system",
|
|
391
|
+
defaultDatasource: "cloud",
|
|
392
|
+
name: "Organization Scoping Plugin",
|
|
393
|
+
description: "Row-level Organization isolation: auto-stamps `organization_id` on insert from `ExecutionContext.tenantId`, replays seed datasets per new org, and bootstraps a default organization for the first platform admin."
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
// src/org-scoping-plugin.ts
|
|
397
|
+
var OrgScopingPlugin = class {
|
|
398
|
+
constructor(options = {}) {
|
|
399
|
+
this.name = "com.objectstack.org-scoping";
|
|
400
|
+
this.type = "standard";
|
|
401
|
+
this.version = "1.0.0";
|
|
402
|
+
this.dependencies = ["com.objectstack.engine.objectql"];
|
|
403
|
+
/** Per-object field-name cache; same shape as SecurityPlugin's. */
|
|
404
|
+
this.fieldNamesCache = /* @__PURE__ */ new Map();
|
|
405
|
+
this.opts = {
|
|
406
|
+
ensureDefaultOrganization: options.ensureDefaultOrganization !== false
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
async init(ctx) {
|
|
410
|
+
ctx.logger.info("Initializing Org-Scoping Plugin...");
|
|
411
|
+
ctx.registerService("org-scoping", this);
|
|
412
|
+
ctx.getService("manifest").register({
|
|
413
|
+
...orgScopingPluginManifestHeader,
|
|
414
|
+
objects: orgScopingObjects
|
|
415
|
+
});
|
|
416
|
+
ctx.logger.info("Org-Scoping Plugin initialized");
|
|
417
|
+
}
|
|
418
|
+
async start(ctx) {
|
|
419
|
+
ctx.logger.info("Starting Org-Scoping Plugin...");
|
|
420
|
+
let ql;
|
|
421
|
+
let metadata;
|
|
422
|
+
try {
|
|
423
|
+
ql = ctx.getService("objectql");
|
|
424
|
+
try {
|
|
425
|
+
metadata = ctx.getService("metadata");
|
|
426
|
+
} catch {
|
|
427
|
+
metadata = void 0;
|
|
428
|
+
}
|
|
429
|
+
} catch {
|
|
430
|
+
ctx.logger.warn(
|
|
431
|
+
"ObjectQL service not available, org-scoping middleware not registered"
|
|
432
|
+
);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
if (!ql || typeof ql.registerMiddleware !== "function") {
|
|
436
|
+
ctx.logger.warn(
|
|
437
|
+
"ObjectQL engine does not support middleware, org-scoping middleware not registered"
|
|
438
|
+
);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
ql.registerMiddleware(async (opCtx, next) => {
|
|
442
|
+
if (opCtx.context?.isSystem) return next();
|
|
443
|
+
if (opCtx.operation === "insert" && opCtx.data && typeof opCtx.data === "object" && !Array.isArray(opCtx.data) && opCtx.context?.tenantId) {
|
|
444
|
+
const fields = await this.getObjectFieldNames(metadata, opCtx.object, ql);
|
|
445
|
+
if (fields && fields.has("organization_id")) {
|
|
446
|
+
const data = opCtx.data;
|
|
447
|
+
if (data.organization_id == null || data.organization_id === "") {
|
|
448
|
+
data.organization_id = opCtx.context.tenantId;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
await next();
|
|
453
|
+
});
|
|
454
|
+
ql.registerMiddleware(async (opCtx, next) => {
|
|
455
|
+
await next();
|
|
456
|
+
if (opCtx?.object !== "sys_organization" || opCtx?.operation !== "create" && opCtx?.operation !== "insert") {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
const newOrgId = opCtx?.result?.id ?? opCtx?.data?.id;
|
|
460
|
+
if (!newOrgId) return;
|
|
461
|
+
const kernel = ctx.kernel ?? ctx;
|
|
462
|
+
let datasets;
|
|
463
|
+
try {
|
|
464
|
+
const raw = kernel?.getService?.("seed-datasets");
|
|
465
|
+
if (Array.isArray(raw) && raw.length > 0) datasets = raw;
|
|
466
|
+
} catch {
|
|
467
|
+
}
|
|
468
|
+
let orgCount = 0;
|
|
469
|
+
try {
|
|
470
|
+
const allOrgs = await ql.find(
|
|
471
|
+
"sys_organization",
|
|
472
|
+
{ limit: 2, fields: ["id"] },
|
|
473
|
+
{ context: { isSystem: true } }
|
|
474
|
+
);
|
|
475
|
+
const list = Array.isArray(allOrgs) ? allOrgs : Array.isArray(allOrgs?.records) ? allOrgs.records : [];
|
|
476
|
+
orgCount = list.length;
|
|
477
|
+
} catch (e) {
|
|
478
|
+
ctx.logger.warn("[org-scoping] failed to count organizations", {
|
|
479
|
+
error: e.message
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
let replayed = false;
|
|
483
|
+
try {
|
|
484
|
+
const replayer = kernel?.getService?.("seed-replayer");
|
|
485
|
+
if (typeof replayer === "function") {
|
|
486
|
+
const summary = await replayer(newOrgId);
|
|
487
|
+
const total = (summary?.inserted ?? 0) + (summary?.updated ?? 0);
|
|
488
|
+
ctx.logger.info(
|
|
489
|
+
`[org-scoping] per-org seed replay for ${newOrgId}: +${summary?.inserted ?? 0} inserted, ${summary?.updated ?? 0} updated, ${summary?.errors?.length ?? 0} error(s)`,
|
|
490
|
+
{
|
|
491
|
+
organizationId: newOrgId,
|
|
492
|
+
errors: summary?.errors?.slice?.(0, 5)
|
|
493
|
+
}
|
|
494
|
+
);
|
|
495
|
+
if (total > 0) replayed = true;
|
|
496
|
+
} else if (datasets) {
|
|
497
|
+
ctx.logger.warn(
|
|
498
|
+
"[org-scoping] per-org seed: datasets present but no replayer registered",
|
|
499
|
+
{ organizationId: newOrgId }
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
} catch (e) {
|
|
503
|
+
ctx.logger.warn(
|
|
504
|
+
"[org-scoping] per-org seed replay failed, falling back",
|
|
505
|
+
{ organizationId: newOrgId, error: e.message }
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
if (replayed) return;
|
|
509
|
+
if (orgCount === 1) {
|
|
510
|
+
try {
|
|
511
|
+
const claims = await claimOrphanOrgRows(ql, newOrgId, { logger: ctx.logger });
|
|
512
|
+
if (claims.length > 0) {
|
|
513
|
+
const total = claims.reduce((s, c) => s + c.count, 0);
|
|
514
|
+
ctx.logger.info(
|
|
515
|
+
`[org-scoping] claimed ${total} orphan seed row(s) for first organization ${newOrgId}`,
|
|
516
|
+
{ breakdown: claims }
|
|
517
|
+
);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
} catch (e) {
|
|
521
|
+
ctx.logger.warn("[org-scoping] claim-orphan-org-rows failed", {
|
|
522
|
+
error: e.message
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
if (orgCount > 1) {
|
|
527
|
+
try {
|
|
528
|
+
const summary = await cloneOrgSeedData(ql, newOrgId, { logger: ctx.logger });
|
|
529
|
+
if (summary.length > 0) {
|
|
530
|
+
const total = summary.reduce((s, c) => s + c.count, 0);
|
|
531
|
+
ctx.logger.info(
|
|
532
|
+
`[org-scoping] cloned ${total} seed row(s) for new organization ${newOrgId}`,
|
|
533
|
+
{ breakdown: summary }
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
} catch (e) {
|
|
537
|
+
ctx.logger.warn("[org-scoping] clone-org-seed-data failed", {
|
|
538
|
+
organizationId: newOrgId,
|
|
539
|
+
error: e.message
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
if (this.opts.ensureDefaultOrganization) {
|
|
545
|
+
const runEnsure = async () => {
|
|
546
|
+
try {
|
|
547
|
+
const res = await ensureDefaultOrganization(ql, { logger: ctx.logger });
|
|
548
|
+
if (res.defaultOrgCreated) {
|
|
549
|
+
ctx.logger.info(
|
|
550
|
+
`[org-scoping] created Default Organization ${res.defaultOrgId} for platform admin`
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
} catch (e) {
|
|
554
|
+
ctx.logger.warn?.("[org-scoping] ensureDefaultOrganization failed", {
|
|
555
|
+
error: e.message
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
if (typeof ctx.hook === "function") {
|
|
560
|
+
ctx.hook("kernel:ready", runEnsure);
|
|
561
|
+
} else {
|
|
562
|
+
void runEnsure();
|
|
563
|
+
}
|
|
564
|
+
ql.registerMiddleware(async (opCtx, next) => {
|
|
565
|
+
await next();
|
|
566
|
+
if (opCtx?.object === "sys_user_permission_set" && (opCtx?.operation === "insert" || opCtx?.operation === "create")) {
|
|
567
|
+
await runEnsure();
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
ctx.logger.info("Org-Scoping middleware registered on ObjectQL engine");
|
|
572
|
+
}
|
|
573
|
+
async destroy() {
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Resolve the column-name set for an object (mirrors SecurityPlugin's
|
|
577
|
+
* loader so the two plugins behave consistently). Returns `null` if
|
|
578
|
+
* the schema can't be loaded — caller skips injection.
|
|
579
|
+
*/
|
|
580
|
+
async getObjectFieldNames(metadata, objectName, ql) {
|
|
581
|
+
if (this.fieldNamesCache.has(objectName)) {
|
|
582
|
+
return this.fieldNamesCache.get(objectName) ?? null;
|
|
583
|
+
}
|
|
584
|
+
const result = await this.loadObjectFieldNames(metadata, objectName, ql);
|
|
585
|
+
if (result) this.fieldNamesCache.set(objectName, result);
|
|
586
|
+
return result;
|
|
587
|
+
}
|
|
588
|
+
async loadObjectFieldNames(metadata, objectName, ql) {
|
|
589
|
+
try {
|
|
590
|
+
let obj = typeof ql?.getSchema === "function" ? ql.getSchema(objectName) : null;
|
|
591
|
+
if (!obj || !obj.fields) {
|
|
592
|
+
obj = await metadata?.get?.("object", objectName);
|
|
593
|
+
}
|
|
594
|
+
if (!obj || !obj.fields) return null;
|
|
595
|
+
const set = /* @__PURE__ */ new Set(["id"]);
|
|
596
|
+
if (Array.isArray(obj.fields)) {
|
|
597
|
+
for (const f of obj.fields) {
|
|
598
|
+
if (f?.name) set.add(String(f.name));
|
|
599
|
+
}
|
|
600
|
+
} else if (typeof obj.fields === "object") {
|
|
601
|
+
for (const key of Object.keys(obj.fields)) {
|
|
602
|
+
set.add(key);
|
|
603
|
+
const v = obj.fields[key];
|
|
604
|
+
if (v && typeof v === "object" && v.name) set.add(String(v.name));
|
|
605
|
+
}
|
|
606
|
+
} else {
|
|
607
|
+
return null;
|
|
608
|
+
}
|
|
609
|
+
return set;
|
|
610
|
+
} catch {
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
616
|
+
0 && (module.exports = {
|
|
617
|
+
ORG_SCOPING_PLUGIN_ID,
|
|
618
|
+
ORG_SCOPING_PLUGIN_VERSION,
|
|
619
|
+
OrgScopingPlugin,
|
|
620
|
+
claimOrphanOrgRows,
|
|
621
|
+
cloneOrgSeedData,
|
|
622
|
+
ensureDefaultOrganization,
|
|
623
|
+
orgScopingObjects,
|
|
624
|
+
orgScopingPluginManifestHeader
|
|
625
|
+
});
|
|
626
|
+
//# sourceMappingURL=index.js.map
|