@usebetterdev/tenant-core 0.2.0-beta.9 → 0.3.0-beta.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/index.js CHANGED
@@ -16,7 +16,6 @@ function getTelemetryTenantConfig(config) {
16
16
  custom: !!r.custom
17
17
  },
18
18
  tenantTablesCount: 0,
19
- hasGetTenantRepository: !!config.getTenantRepository,
20
19
  loadTenant: config.loadTenant,
21
20
  basePathSet: !!config.basePath,
22
21
  plugins: (config.plugins ?? []).map((p) => String(p.id))
@@ -326,7 +325,6 @@ async function handleRequest(request, next, options) {
326
325
 
327
326
  // src/api.ts
328
327
  var DEFAULT_LIST_LIMIT = 50;
329
- var MAX_LIST_LIMIT = 50;
330
328
  function requireRunAsSystem(adapter) {
331
329
  if (!adapter.runAsSystem) {
332
330
  throw new Error(
@@ -335,17 +333,7 @@ function requireRunAsSystem(adapter) {
335
333
  }
336
334
  return adapter.runAsSystem;
337
335
  }
338
- function requireTenantRepository(getTenantRepository) {
339
- if (!getTenantRepository) {
340
- throw new Error(
341
- "better-tenant: tenant.api requires getTenantRepository in config (adapter provides CRUD for tenants table)"
342
- );
343
- }
344
- return getTenantRepository;
345
- }
346
336
  function createTenantApi(adapter, getTenantRepository) {
347
- const runAsSystem2 = requireRunAsSystem(adapter);
348
- const getRepository = requireTenantRepository(getTenantRepository);
349
337
  return {
350
338
  async createTenant(data) {
351
339
  if (!data.name?.trim()) {
@@ -354,8 +342,9 @@ function createTenantApi(adapter, getTenantRepository) {
354
342
  if (!data.slug?.trim()) {
355
343
  throw new Error("better-tenant: createTenant requires slug");
356
344
  }
345
+ const runAsSystem2 = requireRunAsSystem(adapter);
357
346
  return runAsSystem2(
358
- (database) => getRepository(database).create({
347
+ (database) => getTenantRepository(database).create({
359
348
  name: data.name.trim(),
360
349
  slug: data.slug.trim()
361
350
  })
@@ -365,29 +354,44 @@ function createTenantApi(adapter, getTenantRepository) {
365
354
  if (!tenantId?.trim()) {
366
355
  throw new Error("better-tenant: updateTenant requires tenantId");
367
356
  }
357
+ const runAsSystem2 = requireRunAsSystem(adapter);
368
358
  return runAsSystem2(
369
- (database) => getRepository(database).update(tenantId, {
359
+ (database) => getTenantRepository(database).update(tenantId, {
370
360
  ...data.name !== void 0 && { name: data.name },
371
361
  ...data.slug !== void 0 && { slug: data.slug }
372
362
  })
373
363
  );
374
364
  },
375
365
  async listTenants(options = {}) {
376
- const limit = Math.min(
377
- options.limit ?? DEFAULT_LIST_LIMIT,
378
- MAX_LIST_LIMIT
379
- );
366
+ const limit = options.limit ?? DEFAULT_LIST_LIMIT;
380
367
  const offset = Math.max(0, options.offset ?? 0);
368
+ const runAsSystem2 = requireRunAsSystem(adapter);
381
369
  return runAsSystem2(
382
- (database) => getRepository(database).list({ limit, offset })
370
+ (database) => getTenantRepository(database).list({ limit, offset })
383
371
  );
384
372
  },
385
373
  async deleteTenant(tenantId) {
386
374
  if (!tenantId?.trim()) {
387
375
  throw new Error("better-tenant: deleteTenant requires tenantId");
388
376
  }
377
+ const runAsSystem2 = requireRunAsSystem(adapter);
378
+ return runAsSystem2(
379
+ (database) => getTenantRepository(database).delete(tenantId)
380
+ );
381
+ },
382
+ async getTenant(tenantId) {
383
+ if (!tenantId?.trim()) {
384
+ throw new Error("better-tenant: getTenant requires tenantId");
385
+ }
386
+ const runAsSystem2 = requireRunAsSystem(adapter);
387
+ return runAsSystem2(
388
+ (database) => getTenantRepository(database).getById(tenantId)
389
+ );
390
+ },
391
+ async countTenants() {
392
+ const runAsSystem2 = requireRunAsSystem(adapter);
389
393
  return runAsSystem2(
390
- (database) => getRepository(database).delete(tenantId)
394
+ (database) => getTenantRepository(database).count()
391
395
  );
392
396
  }
393
397
  };
@@ -397,7 +401,7 @@ async function runAs(tenantId, adapter, fn) {
397
401
  }
398
402
  async function runAsSystem(adapter, fn) {
399
403
  const run = requireRunAsSystem(adapter);
400
- return runWithContext({ tenantId: "", isSystem: true }, () => run(fn));
404
+ return runWithContext({ isSystem: true }, () => run(fn));
401
405
  }
402
406
 
403
407
  // src/resolver.ts
@@ -479,39 +483,7 @@ function decodeJwtPayload(token) {
479
483
  return null;
480
484
  }
481
485
  }
482
- function resolveFromJwt(request, config) {
483
- const getToken = request.getToken;
484
- if (!getToken) {
485
- return void 0;
486
- }
487
- const token = typeof getToken === "function" ? getToken() : getToken;
488
- const value = token instanceof Promise ? void 0 : token ?? void 0;
489
- const resolved = value ?? void 0;
490
- if (!resolved) {
491
- return void 0;
492
- }
493
- const claim = typeof config === "string" ? config : config.claim;
494
- if (!claim) {
495
- return void 0;
496
- }
497
- const verifyToken = typeof config === "object" ? config.verifyToken : void 0;
498
- let payload;
499
- if (verifyToken) {
500
- const result = verifyToken(resolved);
501
- if (result instanceof Promise) {
502
- return void 0;
503
- }
504
- payload = result;
505
- } else {
506
- payload = decodeJwtPayload(resolved);
507
- }
508
- if (!payload) {
509
- return void 0;
510
- }
511
- const claimValue = payload[claim];
512
- return typeof claimValue === "string" && claimValue.length > 0 ? claimValue : void 0;
513
- }
514
- async function resolveFromJwtAsync(request, config) {
486
+ async function resolveFromJwt(request, config) {
515
487
  const getToken = request.getToken;
516
488
  if (!getToken) {
517
489
  return void 0;
@@ -539,40 +511,28 @@ async function resolveFromJwtAsync(request, config) {
539
511
  const claimValue = payload[claim];
540
512
  return typeof claimValue === "string" && claimValue.length > 0 ? claimValue : void 0;
541
513
  }
542
- function resolveTenant(request, config) {
514
+ function describeStrategies(config) {
515
+ const strategies = [];
543
516
  if (config.header !== void 0) {
544
- const v = resolveFromHeader(request, config.header);
545
- if (v !== void 0) {
546
- return v;
547
- }
517
+ strategies.push(`header '${config.header}'`);
548
518
  }
549
519
  if (config.path !== void 0) {
550
- const v = resolveFromPath(request, config.path);
551
- if (v !== void 0) {
552
- return v;
553
- }
520
+ const pattern = typeof config.path === "string" ? config.path : config.path.pattern;
521
+ strategies.push(`path '${pattern}'`);
554
522
  }
555
523
  if (config.subdomain !== void 0 && config.subdomain !== false) {
556
- const v = resolveFromSubdomain(request, config.subdomain);
557
- if (v !== void 0) {
558
- return v;
559
- }
524
+ strategies.push("subdomain");
560
525
  }
561
526
  if (config.jwt !== void 0) {
562
- const v = resolveFromJwt(request, config.jwt);
563
- if (v !== void 0) {
564
- return v;
565
- }
527
+ const claim = typeof config.jwt === "string" ? config.jwt : config.jwt.claim;
528
+ strategies.push(`jwt claim '${claim}'`);
566
529
  }
567
530
  if (config.custom !== void 0) {
568
- const v = config.custom(request);
569
- if (typeof v === "string" && v.length > 0) {
570
- return v;
571
- }
531
+ strategies.push("custom resolver");
572
532
  }
573
- return void 0;
533
+ return strategies;
574
534
  }
575
- async function resolveTenantAsync(request, config) {
535
+ async function resolveTenant(request, config) {
576
536
  if (config.header !== void 0) {
577
537
  const v = resolveFromHeader(request, config.header);
578
538
  if (v !== void 0) {
@@ -592,7 +552,7 @@ async function resolveTenantAsync(request, config) {
592
552
  }
593
553
  }
594
554
  if (config.jwt !== void 0) {
595
- const v = await resolveFromJwtAsync(request, config.jwt);
555
+ const v = await resolveFromJwt(request, config.jwt);
596
556
  if (v !== void 0) {
597
557
  return v;
598
558
  }
@@ -606,41 +566,214 @@ async function resolveTenantAsync(request, config) {
606
566
  return void 0;
607
567
  }
608
568
 
569
+ // src/console-endpoints.ts
570
+ function toConsoleRequest(request) {
571
+ if (request && typeof request === "object") {
572
+ return request;
573
+ }
574
+ return {};
575
+ }
576
+ function parsePositiveInt(value, max) {
577
+ if (value === void 0) {
578
+ return void 0;
579
+ }
580
+ const parsed = Number(value);
581
+ if (!Number.isFinite(parsed) || parsed < 0) {
582
+ return void 0;
583
+ }
584
+ return Math.min(Math.floor(parsed), max);
585
+ }
586
+ function createTenantConsoleEndpoints(api) {
587
+ return [
588
+ {
589
+ method: "GET",
590
+ path: "/tenants",
591
+ requiredPermission: "read",
592
+ async handler(request) {
593
+ try {
594
+ const { query } = toConsoleRequest(request);
595
+ const limit = parsePositiveInt(query?.limit, 1e3);
596
+ const offset = parsePositiveInt(query?.offset, Number.MAX_SAFE_INTEGER);
597
+ const options = {};
598
+ if (limit !== void 0) {
599
+ options.limit = limit;
600
+ }
601
+ if (offset !== void 0) {
602
+ options.offset = offset;
603
+ }
604
+ const tenants = await api.listTenants(options);
605
+ return { status: 200, body: tenants };
606
+ } catch (error) {
607
+ return { status: 500, body: { error: "Internal server error" } };
608
+ }
609
+ }
610
+ },
611
+ {
612
+ method: "GET",
613
+ path: "/tenants/:id",
614
+ requiredPermission: "read",
615
+ async handler(request) {
616
+ try {
617
+ const { params } = toConsoleRequest(request);
618
+ const id = params?.id?.trim();
619
+ if (!id) {
620
+ return { status: 400, body: { error: "Missing tenant id" } };
621
+ }
622
+ const tenant = await api.getTenant(id);
623
+ if (!tenant) {
624
+ return { status: 404, body: { error: "Tenant not found" } };
625
+ }
626
+ return { status: 200, body: tenant };
627
+ } catch {
628
+ return { status: 500, body: { error: "Internal server error" } };
629
+ }
630
+ }
631
+ },
632
+ {
633
+ method: "POST",
634
+ path: "/tenants",
635
+ requiredPermission: "write",
636
+ async handler(request) {
637
+ try {
638
+ const { body } = toConsoleRequest(request);
639
+ if (!body || typeof body !== "object") {
640
+ return { status: 400, body: { error: "Request body is required" } };
641
+ }
642
+ const name = typeof body.name === "string" ? body.name : "";
643
+ const slug = typeof body.slug === "string" ? body.slug : "";
644
+ if (!name.trim() || !slug.trim()) {
645
+ return { status: 400, body: { error: "name and slug are required" } };
646
+ }
647
+ const tenant = await api.createTenant({ name, slug });
648
+ return { status: 201, body: tenant };
649
+ } catch {
650
+ return { status: 500, body: { error: "Internal server error" } };
651
+ }
652
+ }
653
+ },
654
+ {
655
+ method: "PATCH",
656
+ path: "/tenants/:id",
657
+ requiredPermission: "write",
658
+ async handler(request) {
659
+ try {
660
+ const { params, body } = toConsoleRequest(request);
661
+ const id = params?.id?.trim();
662
+ if (!id) {
663
+ return { status: 400, body: { error: "Missing tenant id" } };
664
+ }
665
+ if (!body || typeof body !== "object") {
666
+ return { status: 400, body: { error: "Request body is required" } };
667
+ }
668
+ const data = {};
669
+ if (typeof body.name === "string") {
670
+ data.name = body.name;
671
+ }
672
+ if (typeof body.slug === "string") {
673
+ data.slug = body.slug;
674
+ }
675
+ const tenant = await api.updateTenant(id, data);
676
+ return { status: 200, body: tenant };
677
+ } catch {
678
+ return { status: 500, body: { error: "Internal server error" } };
679
+ }
680
+ }
681
+ },
682
+ {
683
+ method: "DELETE",
684
+ path: "/tenants/:id",
685
+ requiredPermission: "admin",
686
+ async handler(request) {
687
+ try {
688
+ const { params } = toConsoleRequest(request);
689
+ const id = params?.id?.trim();
690
+ if (!id) {
691
+ return { status: 400, body: { error: "Missing tenant id" } };
692
+ }
693
+ await api.deleteTenant(id);
694
+ return { status: 204, body: null };
695
+ } catch {
696
+ return { status: 500, body: { error: "Internal server error" } };
697
+ }
698
+ }
699
+ },
700
+ {
701
+ method: "GET",
702
+ path: "/stats",
703
+ requiredPermission: "read",
704
+ async handler() {
705
+ try {
706
+ const tenantCount = await api.countTenants();
707
+ return { status: 200, body: { tenantCount } };
708
+ } catch (error) {
709
+ return { status: 500, body: { error: "Internal server error" } };
710
+ }
711
+ }
712
+ }
713
+ ];
714
+ }
715
+
609
716
  // src/better-tenant.ts
717
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
718
+ async function resolveIdentifierToId(identifier, resolverConfig, adapter, getTenantRepository) {
719
+ if (resolverConfig.resolveToId) {
720
+ return resolverConfig.resolveToId(identifier);
721
+ }
722
+ if (UUID_RE.test(identifier)) {
723
+ return identifier;
724
+ }
725
+ if (adapter.runAsSystem) {
726
+ const tenant = await adapter.runAsSystem(
727
+ (systemDb) => getTenantRepository(systemDb).getBySlug(identifier)
728
+ );
729
+ return tenant?.id;
730
+ }
731
+ return identifier;
732
+ }
610
733
  function betterTenant(config) {
611
- const { adapter, tenantResolver, getTenantRepository, loadTenant } = config;
734
+ const { database, tenantResolver, loadTenant } = config;
735
+ const { adapter, getTenantRepository } = database;
612
736
  sendInitTelemetry(config, config.telemetry);
613
- const api = getTenantRepository ? createTenantApi(adapter, getTenantRepository) : createStubTenantApi();
614
- const shouldLoadTenant = getTenantRepository != null && loadTenant !== false;
615
- const runWithTenantAndDatabaseOptions = shouldLoadTenant ? { loadTenant: true, getTenantRepository } : void 0;
737
+ const api = createTenantApi(adapter, getTenantRepository);
738
+ const runWithTenantAndDatabaseOptions = loadTenant !== false ? { loadTenant: true, getTenantRepository } : void 0;
739
+ const resolverStrategies = describeStrategies(tenantResolver);
740
+ async function resolveAndNormalize(request) {
741
+ const raw = await resolveTenant(request, tenantResolver);
742
+ if (!raw) return void 0;
743
+ return resolveIdentifierToId(
744
+ raw,
745
+ tenantResolver,
746
+ adapter,
747
+ getTenantRepository
748
+ );
749
+ }
750
+ if (config.console) {
751
+ const endpoints = createTenantConsoleEndpoints(api);
752
+ config.console.registerProduct({
753
+ id: "tenant",
754
+ name: "Better Tenant",
755
+ endpoints
756
+ });
757
+ }
616
758
  return {
617
759
  getContext,
618
760
  getDatabase: () => getDatabase(),
619
761
  runWithTenant,
620
- runWithTenantAndDatabase: (tenantId, _adapter, fn) => runWithTenantAndDatabase(tenantId, adapter, fn, runWithTenantAndDatabaseOptions),
621
- runAs: (tenantId, _adapter, fn) => runAs(tenantId, adapter, fn),
762
+ runAs: (tenantId, fn) => runWithTenantAndDatabase(
763
+ tenantId,
764
+ adapter,
765
+ fn,
766
+ runWithTenantAndDatabaseOptions
767
+ ),
622
768
  runAsSystem: (fn) => runAsSystem(adapter, fn),
623
- resolveTenant: (request) => resolveTenant(request, tenantResolver),
624
- resolveTenantAsync: (request) => resolveTenantAsync(request, tenantResolver),
769
+ resolveTenant: resolveAndNormalize,
770
+ resolverStrategies,
625
771
  handleRequest: (request, next, options) => handleRequest(request, next, {
626
772
  ...options,
627
- resolveTenant: (input) => resolveTenantAsync(input, tenantResolver),
773
+ resolveTenant: resolveAndNormalize,
628
774
  adapter
629
775
  }),
630
- tenant: { api }
631
- };
632
- }
633
- function createStubTenantApi() {
634
- const err = () => {
635
- throw new Error(
636
- "better-tenant: tenant.api requires getTenantRepository in config"
637
- );
638
- };
639
- return {
640
- createTenant: () => err(),
641
- updateTenant: () => err(),
642
- listTenants: () => err(),
643
- deleteTenant: () => err()
776
+ api
644
777
  };
645
778
  }
646
779
 
@@ -733,14 +866,13 @@ export {
733
866
  TenantNotResolvedError,
734
867
  betterTenant,
735
868
  createTenantApi,
869
+ describeStrategies,
736
870
  getContext,
737
871
  handleRequest,
738
872
  resolveTenant,
739
- resolveTenantAsync,
740
873
  runAs,
741
874
  runAsSystem,
742
875
  runWithTenant,
743
- runWithTenantAndDatabase,
744
876
  sendCliTelemetry,
745
877
  toResolvableRequest
746
878
  };