@easy-cms/plugin-multi-tenant 0.44.0 → 0.46.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/dist/index.js +114 -18
- package/package.json +7 -4
package/dist/index.js
CHANGED
|
@@ -61,7 +61,7 @@ import {
|
|
|
61
61
|
// src/info.ts
|
|
62
62
|
var INFO = {
|
|
63
63
|
name: "@easy-cms/plugin-multi-tenant",
|
|
64
|
-
...true ? { version: "0.
|
|
64
|
+
...true ? { version: "0.46.0" } : {}
|
|
65
65
|
};
|
|
66
66
|
|
|
67
67
|
// src/members.ts
|
|
@@ -263,7 +263,9 @@ function multiTenantPlugin(options) {
|
|
|
263
263
|
...field,
|
|
264
264
|
blocks: field.blocks.map((b) => ({ ...b, fields: withTenantFilters(b.fields) }))
|
|
265
265
|
};
|
|
266
|
-
|
|
266
|
+
const target = field.type === "relationship" ? field.to : field.type === "upload" ? MEDIA : null;
|
|
267
|
+
if (field.type !== "relationship" && field.type !== "upload" || !target || !scoped.has(target))
|
|
268
|
+
return field;
|
|
267
269
|
const own = field.filterOptions;
|
|
268
270
|
return {
|
|
269
271
|
...field,
|
|
@@ -284,12 +286,31 @@ function multiTenantPlugin(options) {
|
|
|
284
286
|
position: "sidebar",
|
|
285
287
|
label: { en: "Tenant", th: "Tenant" },
|
|
286
288
|
// Set from the tenant the request works in; only users with access to all tenants choose.
|
|
287
|
-
access: { update: ({ user, context }) => superUser(user, context) }
|
|
289
|
+
access: { update: ({ user, context }) => superUser(user, context) },
|
|
290
|
+
admin: {
|
|
291
|
+
// New documents start in the chosen tenant.
|
|
292
|
+
defaultValue: ({ context }) => tenantOf(context).tenant ?? void 0,
|
|
293
|
+
// A column while every tenant is shown.
|
|
294
|
+
column: ({ user, context }) => superUser(user, context) && tenantOf(context).tenant === null,
|
|
295
|
+
allowCreate: false
|
|
296
|
+
}
|
|
288
297
|
};
|
|
289
|
-
const fillTenant = ({ data, operation, context }) => {
|
|
298
|
+
const fillTenant = (fields) => async ({ data, operation, context, cms }) => {
|
|
290
299
|
if (operation !== "create" || (data[TENANT_FIELD] ?? null) !== null) return data;
|
|
291
300
|
const { tenant } = tenantOf(context);
|
|
292
|
-
|
|
301
|
+
if (tenant !== null) return { ...data, [TENANT_FIELD]: tenant };
|
|
302
|
+
for (const field of fields) {
|
|
303
|
+
const target = field.type === "relationship" ? field.to : field.type === "upload" ? MEDIA : null;
|
|
304
|
+
if (!target || !scoped.has(target) || field.name === TENANT_FIELD) continue;
|
|
305
|
+
const value = [data[field.name]].flat()[0];
|
|
306
|
+
const id = value && typeof value === "object" ? value.id : value;
|
|
307
|
+
if (id === null || id === void 0) continue;
|
|
308
|
+
if (!cms.config.collections.some((c) => c.slug === target)) continue;
|
|
309
|
+
const doc = await cms.findById(target, id, { depth: 0, draft: true });
|
|
310
|
+
const owner = doc?.[TENANT_FIELD];
|
|
311
|
+
if (owner !== null && owner !== void 0) return { ...data, [TENANT_FIELD]: owner };
|
|
312
|
+
}
|
|
313
|
+
return data;
|
|
293
314
|
};
|
|
294
315
|
const checkTenant = (slug) => ({ data, operation, originalDoc, context, user }) => {
|
|
295
316
|
const value = data[TENANT_FIELD] ?? null;
|
|
@@ -309,7 +330,15 @@ function multiTenantPlugin(options) {
|
|
|
309
330
|
const scopeCollection = (c, builtin) => {
|
|
310
331
|
const fields = withTenantFilters(
|
|
311
332
|
c.fields.map(
|
|
312
|
-
(f) =>
|
|
333
|
+
(f) => f.unique || f.type === "slug" ? {
|
|
334
|
+
...f,
|
|
335
|
+
uniqueWithin: [
|
|
336
|
+
.../* @__PURE__ */ new Set([
|
|
337
|
+
...f.uniqueWithin === void 0 ? [] : [f.uniqueWithin].flat(),
|
|
338
|
+
TENANT_FIELD
|
|
339
|
+
])
|
|
340
|
+
]
|
|
341
|
+
} : f
|
|
313
342
|
)
|
|
314
343
|
);
|
|
315
344
|
const access = { ...builtin, ...c.access };
|
|
@@ -324,7 +353,7 @@ function multiTenantPlugin(options) {
|
|
|
324
353
|
},
|
|
325
354
|
hooks: {
|
|
326
355
|
...c.hooks,
|
|
327
|
-
beforeValidate: [fillTenant, ...c.hooks?.beforeValidate ?? []],
|
|
356
|
+
beforeValidate: [fillTenant(c.fields), ...c.hooks?.beforeValidate ?? []],
|
|
328
357
|
beforeChange: [...c.hooks?.beforeChange ?? [], checkTenant(c.slug)]
|
|
329
358
|
}
|
|
330
359
|
};
|
|
@@ -387,7 +416,11 @@ function multiTenantPlugin(options) {
|
|
|
387
416
|
labels: { singular: { en: "Tenant", th: "Tenant" }, plural: { en: "Tenants", th: "Tenant" } },
|
|
388
417
|
icon: "building",
|
|
389
418
|
useAsTitle: "name",
|
|
390
|
-
admin: {
|
|
419
|
+
admin: {
|
|
420
|
+
group: "settings",
|
|
421
|
+
// Deleting a tenant deletes its content: its name is typed, and what goes is shown.
|
|
422
|
+
confirmDelete: { typeTitle: true, impact: "/tenant-impact" }
|
|
423
|
+
},
|
|
391
424
|
access: tenantsAccess,
|
|
392
425
|
hooks: {
|
|
393
426
|
afterChange: [({ cms }) => forgetTenants(cms)],
|
|
@@ -453,7 +486,16 @@ function multiTenantPlugin(options) {
|
|
|
453
486
|
if (!hasUsers) collections.push({ slug: "users", fields: [memberships], access: usersAccess() });
|
|
454
487
|
for (const builtin of [MEDIA, MEDIA_FOLDERS]) {
|
|
455
488
|
if (scoped.has(builtin) && !declared.has(builtin))
|
|
456
|
-
collections.push(
|
|
489
|
+
collections.push(
|
|
490
|
+
scopeCollection(
|
|
491
|
+
{
|
|
492
|
+
slug: builtin,
|
|
493
|
+
// The built-in `key` of folders: unique per tenant (merged into the built-in field).
|
|
494
|
+
fields: builtin === MEDIA_FOLDERS ? [{ name: "key", type: "text", unique: true }] : []
|
|
495
|
+
},
|
|
496
|
+
BUILTIN_ACCESS[builtin]
|
|
497
|
+
)
|
|
498
|
+
);
|
|
457
499
|
}
|
|
458
500
|
const globals = (config.globals ?? []).map((g) => {
|
|
459
501
|
const filtered = { ...g, fields: withTenantFilters(g.fields) };
|
|
@@ -480,11 +522,11 @@ function multiTenantPlugin(options) {
|
|
|
480
522
|
);
|
|
481
523
|
const bound = user?.apiKey?.permissions?.context;
|
|
482
524
|
const boundTenant = bound ? tenantOf(bound) : void 0;
|
|
483
|
-
if (user && isSuper(user) && (!boundTenant || boundTenant.tenant === null)) {
|
|
525
|
+
if (user && user.member !== true && isSuper(user) && (!boundTenant || boundTenant.tenant === null)) {
|
|
484
526
|
const chosen2 = asked === ALL ? void 0 : findTenant(list, asked);
|
|
485
527
|
return { user, context: { ...context, tenant: chosen2?.id ?? null, allTenants: true } };
|
|
486
528
|
}
|
|
487
|
-
if (user) {
|
|
529
|
+
if (user && user.member !== true) {
|
|
488
530
|
const mine = membershipsOf(user);
|
|
489
531
|
const member = (t) => t ? mine.find((m) => sameId(m.tenant, t.id)) : void 0;
|
|
490
532
|
const membership = boundTenant && boundTenant.tenant !== null ? mine.find((m) => sameId(m.tenant, boundTenant.tenant)) ?? (isSuper(user) ? { tenant: boundTenant.tenant, role: user.role } : void 0) : member(findTenant(list, asked)) ?? member(byHost) ?? mine.find((m) => list.some((t) => sameId(t.id, m.tenant)));
|
|
@@ -517,6 +559,39 @@ function multiTenantPlugin(options) {
|
|
|
517
559
|
};
|
|
518
560
|
}
|
|
519
561
|
};
|
|
562
|
+
const impactEndpoint = {
|
|
563
|
+
path: "/tenant-impact",
|
|
564
|
+
method: "get",
|
|
565
|
+
handler: async ({ user, context, cms, url }) => {
|
|
566
|
+
if (!superUser(user, context))
|
|
567
|
+
return Response.json({ errors: [{ message: "Not allowed" }] }, { status: 403 });
|
|
568
|
+
const raw = url.searchParams.get("id") ?? "";
|
|
569
|
+
const id = /^\d+$/.test(raw) ? Number(raw) : raw;
|
|
570
|
+
const counts = [];
|
|
571
|
+
for (const slug of scoped) {
|
|
572
|
+
const config2 = cms.config.collections.find((c) => c.slug === slug);
|
|
573
|
+
if (!config2) continue;
|
|
574
|
+
const n = await cms.count(slug, { where: { [TENANT_FIELD]: { equals: id } } });
|
|
575
|
+
if (n === 0) continue;
|
|
576
|
+
const plural = config2.labels?.plural;
|
|
577
|
+
const en = typeof plural === "string" ? plural : plural?.en ?? slug;
|
|
578
|
+
const th = typeof plural === "string" ? plural : plural?.th ?? en;
|
|
579
|
+
counts.push({ en: `${n} ${en}`, th: `${th} ${n} \u0E23\u0E32\u0E22\u0E01\u0E32\u0E23` });
|
|
580
|
+
}
|
|
581
|
+
const members = await cms.count("users", {
|
|
582
|
+
where: { [`${MEMBERSHIPS_FIELD}.tenant`]: { equals: id } }
|
|
583
|
+
});
|
|
584
|
+
return {
|
|
585
|
+
message: counts.length ? {
|
|
586
|
+
en: `Also deleted: ${counts.map((c) => c.en).join(", ")}. ${members} members lose this tenant; their accounts stay.`,
|
|
587
|
+
th: `\u0E08\u0E30\u0E25\u0E1A\u0E44\u0E1B\u0E14\u0E49\u0E27\u0E22: ${counts.map((c) => c.th).join(", ")} \u0E2A\u0E21\u0E32\u0E0A\u0E34\u0E01 ${members} \u0E04\u0E19\u0E08\u0E30\u0E2D\u0E2D\u0E01\u0E08\u0E32\u0E01 tenant \u0E19\u0E35\u0E49 \u0E1A\u0E31\u0E0D\u0E0A\u0E35\u0E22\u0E31\u0E07\u0E2D\u0E22\u0E39\u0E48`
|
|
588
|
+
} : {
|
|
589
|
+
en: `It has no content. ${members} members lose this tenant; their accounts stay.`,
|
|
590
|
+
th: `\u0E44\u0E21\u0E48\u0E21\u0E35\u0E40\u0E19\u0E37\u0E49\u0E2D\u0E2B\u0E32 \u0E2A\u0E21\u0E32\u0E0A\u0E34\u0E01 ${members} \u0E04\u0E19\u0E08\u0E30\u0E2D\u0E2D\u0E01\u0E08\u0E32\u0E01 tenant \u0E19\u0E35\u0E49 \u0E1A\u0E31\u0E0D\u0E0A\u0E35\u0E22\u0E31\u0E07\u0E2D\u0E22\u0E39\u0E48`
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
};
|
|
520
595
|
const assign = {
|
|
521
596
|
name: "tenants:assign",
|
|
522
597
|
description: "Give documents without a tenant to one (after adding the plugin to a site)",
|
|
@@ -535,24 +610,34 @@ of its own. Run it once after adding the plugin to a site that has content.
|
|
|
535
610
|
}
|
|
536
611
|
for (const slug of scoped) {
|
|
537
612
|
if (!cms.config.collections.some((c) => c.slug === slug)) continue;
|
|
538
|
-
|
|
539
|
-
for (; ; ) {
|
|
540
|
-
const { docs } = await cms.find(slug, {
|
|
613
|
+
const ids = [];
|
|
614
|
+
for (let page = 1; ; page++) {
|
|
615
|
+
const { docs, hasNextPage } = await cms.find(slug, {
|
|
541
616
|
where: { [TENANT_FIELD]: { exists: false } },
|
|
542
|
-
|
|
617
|
+
sort: "id",
|
|
618
|
+
limit: 500,
|
|
619
|
+
page,
|
|
543
620
|
depth: 0,
|
|
544
621
|
draft: true
|
|
545
622
|
});
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
623
|
+
ids.push(...docs.map((d) => d.id));
|
|
624
|
+
if (!hasNextPage) break;
|
|
625
|
+
}
|
|
626
|
+
let updated = 0;
|
|
627
|
+
const skipped = [];
|
|
628
|
+
for (const id of ids) {
|
|
629
|
+
try {
|
|
630
|
+
await cms.update(slug, id, { [TENANT_FIELD]: tenant.id }, {
|
|
549
631
|
depth: 0,
|
|
550
632
|
live: true
|
|
551
633
|
});
|
|
552
634
|
updated++;
|
|
635
|
+
} catch (error) {
|
|
636
|
+
skipped.push(`${slug} ${id}: ${error.message}`);
|
|
553
637
|
}
|
|
554
638
|
}
|
|
555
639
|
log(`${slug}: ${updated} given to ${tenant.name}`);
|
|
640
|
+
for (const line of skipped) log(` not given: ${line}`);
|
|
556
641
|
}
|
|
557
642
|
const context = { tenant: tenant.id, allTenants: false };
|
|
558
643
|
for (const slug of perTenant) {
|
|
@@ -575,9 +660,20 @@ of its own. Run it once after adding the plugin to a site that has content.
|
|
|
575
660
|
endpoints: [
|
|
576
661
|
...config.endpoints ?? [],
|
|
577
662
|
optionsEndpoint,
|
|
663
|
+
impactEndpoint,
|
|
578
664
|
...membersEndpoints({ tenantsSlug, superUser })
|
|
579
665
|
],
|
|
580
666
|
commands: [...config.commands ?? [], assign],
|
|
667
|
+
// Audit log entries belong to their tenant: its admins see them.
|
|
668
|
+
...config.audit ? {
|
|
669
|
+
audit: {
|
|
670
|
+
...config.audit === true ? {} : config.audit,
|
|
671
|
+
scope: (context) => {
|
|
672
|
+
const { tenant } = tenantOf(context);
|
|
673
|
+
return tenant === null ? null : String(tenant);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
} : {},
|
|
581
677
|
admin: {
|
|
582
678
|
...config.admin,
|
|
583
679
|
modules: [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@easy-cms/plugin-multi-tenant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.46.0",
|
|
4
4
|
"description": "Multi-tenant Easy CMS: several sites or clients in one CMS, each with its own content, members and roles",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -17,11 +17,14 @@
|
|
|
17
17
|
"dist"
|
|
18
18
|
],
|
|
19
19
|
"peerDependencies": {
|
|
20
|
-
"@easy-cms/core": "^0.
|
|
20
|
+
"@easy-cms/core": "^0.46.0"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@easy-cms/
|
|
24
|
-
"@easy-cms/
|
|
23
|
+
"@easy-cms/core": "^0.46.0",
|
|
24
|
+
"@easy-cms/db-sqlite": "^0.46.0",
|
|
25
|
+
"@easy-cms/plugin-form-builder": "^0.46.0",
|
|
26
|
+
"@easy-cms/plugin-nested-docs": "^0.46.0",
|
|
27
|
+
"@easy-cms/plugin-redirects": "^0.46.0"
|
|
25
28
|
},
|
|
26
29
|
"engines": {
|
|
27
30
|
"node": ">=22.12"
|