@easy-cms/plugin-multi-tenant 0.48.0 → 0.49.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.d.ts CHANGED
@@ -34,7 +34,7 @@ interface Member {
34
34
  readonly role: string;
35
35
  }
36
36
 
37
- interface MultiTenantOptions<S extends string = string, T extends string = string> {
37
+ interface MultiTenantPluginOptions<S extends string = string, T extends string = string> {
38
38
  /**
39
39
  * Collections whose documents belong to one tenant, by slug, e.g. `['posts', 'pages',
40
40
  * 'media']`. With `media` and `upload.folders`, the media folders too. Others are shared.
@@ -49,7 +49,9 @@ interface MultiTenantOptions<S extends string = string, T extends string = strin
49
49
  */
50
50
  readonly editShared?: readonly string[];
51
51
  /** Slug of the tenants collection. Default `tenants`. */
52
- readonly tenantsSlug?: T;
52
+ readonly slugs?: {
53
+ readonly tenants?: T;
54
+ };
53
55
  /**
54
56
  * Users who see and manage every tenant and the whole system. Default: users whose own role
55
57
  * is `admin`.
@@ -129,24 +131,25 @@ type MultiTenantPluginTypes<S extends string, T extends string = 'tenants'> = {
129
131
  * and globals with a value per tenant. Requests name their tenant with a header, `?tenant=`,
130
132
  * the admin's cookie or the domain.
131
133
  */
132
- declare function multiTenantPlugin<const S extends string, const T extends string = 'tenants'>(options: MultiTenantOptions<S, T>): TypedPlugin<MultiTenantPluginTypes<S, T>>;
134
+ declare function multiTenantPlugin<const S extends string, const T extends string = 'tenants'>(options: MultiTenantPluginOptions<S, T>): TypedPlugin<MultiTenantPluginTypes<S, T>>;
133
135
 
136
+ /** A tenant by its slug or one of its domains. */
137
+ interface TenantLookup {
138
+ readonly slug?: string;
139
+ readonly host?: string;
140
+ /** Slug of the tenants collection, when the plugin's `slugs.tenants` changed it. */
141
+ readonly collection?: string;
142
+ }
134
143
  /**
135
144
  * The tenant of a page of your site, by slug or by domain (`domains` of the tenant), e.g. from
136
145
  * the request's host. `null` when none matches.
137
146
  */
138
- declare function findTenantFor(cms: EasyCMS, by: {
139
- readonly slug?: string;
140
- readonly host?: string;
141
- }, tenantsSlug?: string): Promise<TenantRef | null>;
147
+ declare function findTenantFor(cms: EasyCMS, by: TenantLookup): Promise<TenantRef | null>;
142
148
  /**
143
149
  * The `context` for Local API calls that read one tenant's content, e.g.
144
150
  * `cms.find('posts', { overrideAccess: false, user: null, context: await tenantContext(cms, { host }) })`.
145
151
  * A tenant that doesn't exist gives a context that finds nothing.
146
152
  */
147
- declare function tenantContext(cms: EasyCMS, by: {
148
- readonly slug?: string;
149
- readonly host?: string;
150
- }, tenantsSlug?: string): Promise<RequestContext>;
153
+ declare function tenantContext(cms: EasyCMS, by: TenantLookup): Promise<RequestContext>;
151
154
 
152
- export { ALL, type Member, type Membership, type MultiTenantOptions, type MultiTenantPluginTypes, type TenantContext, type TenantRef, findTenantFor, multiTenantPlugin, tenantContext, tenantOf };
155
+ export { ALL, type Member, type Membership, type MultiTenantPluginOptions, type MultiTenantPluginTypes, type TenantContext, type TenantLookup, type TenantRef, findTenantFor, multiTenantPlugin, tenantContext, tenantOf };
package/dist/index.js CHANGED
@@ -52,16 +52,17 @@ var tenantOfHost = (list, host) => {
52
52
  // src/plugin.ts
53
53
  import {
54
54
  definePlugin,
55
- isLoggedIn,
56
- MEDIA,
57
- MEDIA_FOLDERS,
55
+ isStaff,
58
56
  ValidationError as ValidationError2
59
57
  } from "@easy-cms/core";
58
+ import { checkRenamedOptions, MEDIA, MEDIA_FOLDERS } from "@easy-cms/core/internal";
60
59
 
61
60
  // src/info.ts
62
61
  var INFO = {
63
62
  name: "@easy-cms/plugin-multi-tenant",
64
- ...true ? { version: "0.48.0" } : {}
63
+ // The plugin API it is written for (`PLUGIN_API_VERSION` of @easy-cms/core).
64
+ apiVersion: 1,
65
+ ...true ? { version: "0.49.0" } : {}
65
66
  };
66
67
 
67
68
  // src/members.ts
@@ -234,8 +235,8 @@ function membersEndpoints(options) {
234
235
 
235
236
  // src/plugin.ts
236
237
  var BUILTIN_ACCESS = {
237
- [MEDIA]: { read: () => true, create: isLoggedIn, update: isLoggedIn, delete: isLoggedIn },
238
- [MEDIA_FOLDERS]: { read: isLoggedIn, create: isLoggedIn, update: isLoggedIn, delete: isLoggedIn }
238
+ [MEDIA]: { read: () => true, create: isStaff, update: isStaff, delete: isStaff },
239
+ [MEDIA_FOLDERS]: { read: isStaff, create: isStaff, update: isStaff, delete: isStaff }
239
240
  };
240
241
  function readCookie(header, name) {
241
242
  for (const part of header?.split(";") ?? []) {
@@ -246,8 +247,9 @@ function readCookie(header, name) {
246
247
  return void 0;
247
248
  }
248
249
  function multiTenantPlugin(options) {
250
+ checkRenamedOptions("multiTenantPlugin", options, { tenantsSlug: "slugs.tenants" });
249
251
  return definePlugin((config) => {
250
- const tenantsSlug = options.tenantsSlug ?? "tenants";
252
+ const tenantsSlug = options.slugs?.tenants ?? "tenants";
251
253
  const header = (options.header ?? "x-easy-cms-tenant").toLowerCase();
252
254
  const cookie = options.cookie ?? "ecms-tenant";
253
255
  const publicReads = options.publicReads ?? "none";
@@ -275,7 +277,7 @@ function multiTenantPlugin(options) {
275
277
  const superUser = (user, _context) => !!user && user.member !== true && user.scoped !== true && isSuper(user);
276
278
  const everywhere = (user, context) => tenantOf(context).allTenants || superUser(user, context);
277
279
  const editShared = new Set(options.editShared ?? []);
278
- const sharedWrite = (slug, original) => async (args) => args.user?.scoped === true && !editShared.has(slug) ? false : (original ?? isLoggedIn)(args);
280
+ const sharedWrite = (slug, original) => async (args) => args.user?.scoped === true && !editShared.has(slug) ? false : (original ?? isStaff)(args);
279
281
  const tenantFilter = (args, op) => {
280
282
  const { tenant } = tenantOf(args.context);
281
283
  if (op === "create") return tenant !== null || superUser(args.user, args.context);
@@ -284,7 +286,7 @@ function multiTenantPlugin(options) {
284
286
  return superUser(args.user, args.context);
285
287
  };
286
288
  const narrow = (original, op) => async (args) => {
287
- const base = await (original ?? isLoggedIn)(args);
289
+ const base = await (original ?? isStaff)(args);
288
290
  if (base === false) return false;
289
291
  const filter = tenantFilter(args, op);
290
292
  if (filter === false) return false;
@@ -319,13 +321,13 @@ function multiTenantPlugin(options) {
319
321
  type: "relationship",
320
322
  to: tenantsSlug,
321
323
  index: true,
322
- position: "sidebar",
323
324
  label: { en: "Tenant", th: "Tenant" },
324
325
  // Set from the tenant the request works in; only users with access to all tenants choose.
325
326
  access: { update: ({ user, context }) => superUser(user, context) },
326
327
  admin: {
328
+ position: "sidebar",
327
329
  // New documents start in the chosen tenant.
328
- defaultValue: ({ context }) => tenantOf(context).tenant ?? void 0,
330
+ initialValue: ({ context }) => tenantOf(context).tenant ?? void 0,
329
331
  // A column while every tenant is shown.
330
332
  column: ({ user, context }) => superUser(user, context) && tenantOf(context).tenant === null,
331
333
  allowCreate: false
@@ -348,7 +350,7 @@ function multiTenantPlugin(options) {
348
350
  }
349
351
  return data;
350
352
  };
351
- const checkTenant = (slug) => ({ data, operation, originalDoc, context, user }) => {
353
+ const checkTenant = (slug) => ({ data, operation, previousDoc, context, user }) => {
352
354
  const value = data[TENANT_FIELD] ?? null;
353
355
  const { tenant } = tenantOf(context);
354
356
  if (!user && tenant === null) return data;
@@ -358,7 +360,7 @@ function multiTenantPlugin(options) {
358
360
  if (value === null) fail("is required: choose a tenant");
359
361
  if (superUser(user, context)) return data;
360
362
  if (!sameId(value, tenant)) fail("must be the tenant you work in");
361
- const before = originalDoc?.[TENANT_FIELD] ?? null;
363
+ const before = previousDoc?.[TENANT_FIELD] ?? null;
362
364
  if (operation === "update" && before !== null && !sameId(before, value))
363
365
  fail("can't move to another tenant");
364
366
  return data;
@@ -451,9 +453,9 @@ function multiTenantPlugin(options) {
451
453
  const tenantsCollection = {
452
454
  slug: tenantsSlug,
453
455
  labels: { singular: { en: "Tenant", th: "Tenant" }, plural: { en: "Tenants", th: "Tenant" } },
454
- icon: "building",
455
456
  useAsTitle: "name",
456
457
  admin: {
458
+ icon: "building",
457
459
  group: "settings.people",
458
460
  // Deleting a tenant deletes its content: its name is typed, and what goes is shown.
459
461
  confirmDelete: { typeTitle: true, impact: "/tenant-impact" }
@@ -562,9 +564,7 @@ function multiTenantPlugin(options) {
562
564
  });
563
565
  const previous = config.onRequest;
564
566
  const onRequest = async (args) => {
565
- const before = previous ? await previous(args) : void 0;
566
- const user = before?.user !== void 0 ? before.user : args.user;
567
- const context = { ...before?.context };
567
+ const user = args.user;
568
568
  const list = await tenantList(args.cms, tenantsSlug);
569
569
  const asked = args.headers.get(header) ?? args.url?.searchParams.get("tenant") ?? readCookie(args.headers.get("cookie"), cookie) ?? null;
570
570
  const byHost = tenantOfHost(
@@ -575,7 +575,7 @@ function multiTenantPlugin(options) {
575
575
  const boundTenant = bound ? tenantOf(bound) : void 0;
576
576
  if (user && user.member !== true && isSuper(user) && (!boundTenant || boundTenant.tenant === null)) {
577
577
  const chosen2 = asked === ALL ? void 0 : findTenant(list, asked);
578
- return { user, context: { ...context, tenant: chosen2?.id ?? null, allTenants: true } };
578
+ return { user, context: { tenant: chosen2?.id ?? null, allTenants: true } };
579
579
  }
580
580
  if (user && user.member !== true) {
581
581
  const mine = membershipsOf(user);
@@ -583,14 +583,13 @@ function multiTenantPlugin(options) {
583
583
  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)));
584
584
  return {
585
585
  user: { ...user, role: membership?.role ?? user.role, scoped: true },
586
- context: { ...context, tenant: membership?.tenant ?? null, allTenants: false }
586
+ context: { tenant: membership?.tenant ?? null, allTenants: false }
587
587
  };
588
588
  }
589
589
  const chosen = asked && asked !== ALL ? findTenant(list, asked) : byHost;
590
590
  return {
591
591
  user,
592
592
  context: {
593
- ...context,
594
593
  tenant: chosen?.id ?? null,
595
594
  allTenants: !chosen && publicReads === "all"
596
595
  }
@@ -707,19 +706,19 @@ of its own. Run it once after adding the plugin to a site that has content.
707
706
  ...config,
708
707
  collections,
709
708
  globals,
710
- onRequest,
709
+ onRequest: [...typeof previous === "function" ? [previous] : previous ?? [], onRequest],
711
710
  endpoints: [
712
711
  ...config.endpoints ?? [],
713
712
  optionsEndpoint,
714
713
  impactEndpoint,
715
714
  ...membersEndpoints({ tenantsSlug, superUser, isSuper })
716
715
  ],
717
- commands: [...config.commands ?? [], assign],
716
+ cliCommands: [...config.cliCommands ?? [], assign],
718
717
  // Audit log entries belong to their tenant: its admins see them.
719
718
  ...config.audit ? {
720
719
  audit: {
721
720
  ...config.audit === true ? {} : config.audit,
722
- scope: (context) => {
721
+ scope: ({ context }) => {
723
722
  const { tenant } = tenantOf(context);
724
723
  return tenant === null ? null : String(tenant);
725
724
  }
@@ -778,12 +777,12 @@ async function removeTenant(cms, id, collections) {
778
777
  }
779
778
 
780
779
  // src/index.ts
781
- async function findTenantFor(cms, by, tenantsSlug = "tenants") {
782
- const list = await tenantList(cms, tenantsSlug);
780
+ async function findTenantFor(cms, by) {
781
+ const list = await tenantList(cms, by.collection ?? "tenants");
783
782
  return (by.slug ? findTenant(list, by.slug) : void 0) ?? tenantOfHost(list, by.host) ?? null;
784
783
  }
785
- async function tenantContext(cms, by, tenantsSlug = "tenants") {
786
- const tenant = await findTenantFor(cms, by, tenantsSlug);
784
+ async function tenantContext(cms, by) {
785
+ const tenant = await findTenantFor(cms, by);
787
786
  return { tenant: tenant?.id ?? null, allTenants: false };
788
787
  }
789
788
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easy-cms/plugin-multi-tenant",
3
- "version": "0.48.0",
3
+ "version": "0.49.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,14 +17,14 @@
17
17
  "dist"
18
18
  ],
19
19
  "peerDependencies": {
20
- "@easy-cms/core": "^0.48.0"
20
+ "@easy-cms/core": "^0.49.0"
21
21
  },
22
22
  "devDependencies": {
23
- "@easy-cms/core": "^0.48.0",
24
- "@easy-cms/db-sqlite": "^0.48.0",
25
- "@easy-cms/plugin-form-builder": "^0.48.0",
26
- "@easy-cms/plugin-nested-docs": "^0.48.0",
27
- "@easy-cms/plugin-redirects": "^0.48.0"
23
+ "@easy-cms/core": "^0.49.0",
24
+ "@easy-cms/db-sqlite": "^0.49.0",
25
+ "@easy-cms/plugin-form-builder": "^0.49.0",
26
+ "@easy-cms/plugin-nested-docs": "^0.49.0",
27
+ "@easy-cms/plugin-redirects": "^0.49.0"
28
28
  },
29
29
  "engines": {
30
30
  "node": ">=22.12"