@happyvertical/smrt-tenancy 0.30.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/AGENTS.md +71 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +7 -0
- package/README.md +122 -0
- package/dist/__smrt-register__.d.ts +2 -0
- package/dist/__smrt-register__.d.ts.map +1 -0
- package/dist/adapters/cli.d.ts +178 -0
- package/dist/adapters/cli.d.ts.map +1 -0
- package/dist/adapters/express.d.ts +115 -0
- package/dist/adapters/express.d.ts.map +1 -0
- package/dist/adapters/index.d.ts +22 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +7 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/sveltekit.d.ts +123 -0
- package/dist/adapters/sveltekit.d.ts.map +1 -0
- package/dist/chunks/context-B5CKsmMi.js +190 -0
- package/dist/chunks/context-B5CKsmMi.js.map +1 -0
- package/dist/chunks/sveltekit-9eRH1RLw.js +153 -0
- package/dist/chunks/sveltekit-9eRH1RLw.js.map +1 -0
- package/dist/chunks/testing-C_tV23JW.js +487 -0
- package/dist/chunks/testing-C_tV23JW.js.map +1 -0
- package/dist/context.d.ts +435 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/decorators.d.ts +126 -0
- package/dist/decorators.d.ts.map +1 -0
- package/dist/enabled-state.d.ts +25 -0
- package/dist/enabled-state.d.ts.map +1 -0
- package/dist/entry-point.d.ts +83 -0
- package/dist/entry-point.d.ts.map +1 -0
- package/dist/fields.d.ts +104 -0
- package/dist/fields.d.ts.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +108 -0
- package/dist/index.js.map +1 -0
- package/dist/interceptor.d.ts +156 -0
- package/dist/interceptor.d.ts.map +1 -0
- package/dist/manifest.json +11 -0
- package/dist/playground.d.ts +2 -0
- package/dist/playground.d.ts.map +1 -0
- package/dist/playground.js +80 -0
- package/dist/playground.js.map +1 -0
- package/dist/registry.d.ts +145 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/smrt-knowledge.json +65 -0
- package/dist/svelte/components/TenantCard.svelte +272 -0
- package/dist/svelte/components/TenantCard.svelte.d.ts +18 -0
- package/dist/svelte/components/TenantCard.svelte.d.ts.map +1 -0
- package/dist/svelte/components/TenantSwitcher.svelte +68 -0
- package/dist/svelte/components/TenantSwitcher.svelte.d.ts +11 -0
- package/dist/svelte/components/TenantSwitcher.svelte.d.ts.map +1 -0
- package/dist/svelte/i18n.d.ts +5 -0
- package/dist/svelte/i18n.d.ts.map +1 -0
- package/dist/svelte/i18n.js +9 -0
- package/dist/svelte/index.d.ts +15 -0
- package/dist/svelte/index.d.ts.map +1 -0
- package/dist/svelte/index.js +19 -0
- package/dist/svelte/playground.d.ts +70 -0
- package/dist/svelte/playground.d.ts.map +1 -0
- package/dist/svelte/playground.js +75 -0
- package/dist/testing.d.ts +145 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +11 -0
- package/dist/testing.js.map +1 -0
- package/dist/ui.d.ts +21 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +33 -0
- package/dist/ui.js.map +1 -0
- package/package.json +99 -0
package/dist/fields.d.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tenancy Field Types and Utilities
|
|
3
|
+
*
|
|
4
|
+
* This module provides types and utility functions for tenant ID fields.
|
|
5
|
+
* The actual field decorator is in decorators.ts.
|
|
6
|
+
*
|
|
7
|
+
* @see https://github.com/happyvertical/smrt/issues/675
|
|
8
|
+
* @see https://github.com/happyvertical/smrt/issues/829
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Options for the `@tenantId()` property decorator.
|
|
12
|
+
*
|
|
13
|
+
* Controls how the decorated field interacts with the tenancy interceptor.
|
|
14
|
+
* All options default to the strictest safe values: auto-filter on, required,
|
|
15
|
+
* auto-populate on, not nullable.
|
|
16
|
+
*
|
|
17
|
+
* @see tenantId
|
|
18
|
+
* @see TenantScopedOptions
|
|
19
|
+
*/
|
|
20
|
+
export interface TenantIdFieldOptions {
|
|
21
|
+
/**
|
|
22
|
+
* Auto-filter queries by this field
|
|
23
|
+
* @default true
|
|
24
|
+
*/
|
|
25
|
+
autoFilter?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Require this field to have a value on save
|
|
28
|
+
* @default true
|
|
29
|
+
*/
|
|
30
|
+
required?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Auto-populate from context on create if not set
|
|
33
|
+
* @default true
|
|
34
|
+
*/
|
|
35
|
+
autoPopulate?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Allow null values (for global resources)
|
|
38
|
+
* @default false
|
|
39
|
+
*/
|
|
40
|
+
nullable?: boolean;
|
|
41
|
+
}
|
|
42
|
+
export declare const TENANT_ID_SYMBOL: unique symbol;
|
|
43
|
+
/**
|
|
44
|
+
* Internal field descriptor stored in `ObjectRegistry` when `@tenantId()` is
|
|
45
|
+
* applied to a property.
|
|
46
|
+
*
|
|
47
|
+
* Consumers should use `isTenantIdField()` and `getTenantIdFieldOptions()`
|
|
48
|
+
* to inspect these descriptors rather than reading the raw properties directly.
|
|
49
|
+
*
|
|
50
|
+
* @see isTenantIdField
|
|
51
|
+
* @see getTenantIdFieldOptions
|
|
52
|
+
*/
|
|
53
|
+
export interface TenantIdFieldDefinition {
|
|
54
|
+
/** Field type marker */
|
|
55
|
+
type: 'foreignKey';
|
|
56
|
+
/** Reference to Tenant class (placeholder - actual class resolved at runtime) */
|
|
57
|
+
reference: 'Tenant';
|
|
58
|
+
/** SQL type */
|
|
59
|
+
sqlType: 'UUID';
|
|
60
|
+
/** Field is required */
|
|
61
|
+
required: boolean;
|
|
62
|
+
/** Field allows null */
|
|
63
|
+
nullable: boolean;
|
|
64
|
+
/** Tenancy-specific options */
|
|
65
|
+
__tenancy: TenantIdFieldOptions & {
|
|
66
|
+
isTenantIdField: true;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Return `true` if the given field definition was produced by the `@tenantId()`
|
|
71
|
+
* decorator (i.e., it has an `__tenancy.isTenantIdField` marker).
|
|
72
|
+
*
|
|
73
|
+
* Used internally by the interceptor and code generators to locate the tenant
|
|
74
|
+
* ID field on a class without knowing its property name in advance.
|
|
75
|
+
*
|
|
76
|
+
* @param field - A raw field definition object, typically from `ObjectRegistry`.
|
|
77
|
+
* @returns `true` if `field` is a tenant ID field definition, `false` otherwise.
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```typescript
|
|
81
|
+
* const fields = ObjectRegistry.getFields('Document');
|
|
82
|
+
* const tenantField = Object.entries(fields).find(([, def]) => isTenantIdField(def));
|
|
83
|
+
* ```
|
|
84
|
+
*
|
|
85
|
+
* @see getTenantIdFieldOptions
|
|
86
|
+
* @see TenantIdFieldDefinition
|
|
87
|
+
*/
|
|
88
|
+
export declare function isTenantIdField(field: unknown): boolean;
|
|
89
|
+
/**
|
|
90
|
+
* Extract the `TenantIdFieldOptions` from a field definition.
|
|
91
|
+
*
|
|
92
|
+
* Returns the tenancy-specific options (autoFilter, required, autoPopulate,
|
|
93
|
+
* nullable) stored inside the field descriptor's `__tenancy` property.
|
|
94
|
+
* Returns `null` if the field was not produced by `@tenantId()`.
|
|
95
|
+
*
|
|
96
|
+
* @param field - A raw field definition object, typically from `ObjectRegistry`.
|
|
97
|
+
* @returns The `TenantIdFieldOptions` if the field is a tenant ID field,
|
|
98
|
+
* `null` otherwise.
|
|
99
|
+
*
|
|
100
|
+
* @see isTenantIdField
|
|
101
|
+
* @see TenantIdFieldOptions
|
|
102
|
+
*/
|
|
103
|
+
export declare function getTenantIdFieldOptions(field: unknown): TenantIdFieldOptions | null;
|
|
104
|
+
//# sourceMappingURL=fields.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fields.d.ts","sourceRoot":"","sources":["../src/fields.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;;;;;GASG;AACH,MAAM,WAAW,oBAAoB;IACnC;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAGD,eAAO,MAAM,gBAAgB,eAAqB,CAAC;AAEnD;;;;;;;;;GASG;AACH,MAAM,WAAW,uBAAuB;IACtC,wBAAwB;IACxB,IAAI,EAAE,YAAY,CAAC;IACnB,iFAAiF;IACjF,SAAS,EAAE,QAAQ,CAAC;IACpB,eAAe;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,wBAAwB;IACxB,QAAQ,EAAE,OAAO,CAAC;IAClB,wBAAwB;IACxB,QAAQ,EAAE,OAAO,CAAC;IAClB,+BAA+B;IAC/B,SAAS,EAAE,oBAAoB,GAAG;QAAE,eAAe,EAAE,IAAI,CAAA;KAAE,CAAC;CAC7D;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAOvD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,OAAO,GACb,oBAAoB,GAAG,IAAI,CAM7B"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { type CliContextOptions, createCliContext, createExpressMiddleware, createSvelteKitHandle, type ExpressMiddlewareOptions, type SvelteKitHandleOptions, } from './adapters/index.js';
|
|
2
|
+
export { enterTenantContext, getCurrentTenant, getTenantId, hasTenantContext, isSuperAdminBypass, isSystemContext, type MinimalTenantContext, requireTenant, requireTenantId, TenantContext, type TenantContextData, TenantContextError, TenantIsolationError, withSuperAdminBypass, withSystemContext, withTenant, withTenantSync, } from './context.js';
|
|
3
|
+
export { TenantScoped, type TenantScopedOptions, tenantId, } from './decorators.js';
|
|
4
|
+
export { runTenantScopedEntryPoint, type TenantEntryPointOptions, } from './entry-point.js';
|
|
5
|
+
export { getTenantIdFieldOptions, isTenantIdField, type TenantIdFieldDefinition, type TenantIdFieldOptions, } from './fields.js';
|
|
6
|
+
export { createTenantInterceptor, disableTenancy, enableTenancy, isTenancyEnabled, type RawQueryPolicy, type TenantInterceptorOptions, } from './interceptor.js';
|
|
7
|
+
export { clearTenantScopedRegistry, getAllTenantScopedClasses, getTenantScopedConfig, isTenantScopedClass, registerTenantScopedClass, type TenantScopedConfig, unregisterTenantScopedClass, } from './registry.js';
|
|
8
|
+
export { assertTenantContextRequired, assertTenantIsolationViolation, createTestTenantContext, resetTenancy, type SetupTestTenancyOptions, setupTestTenancy, testTenantIsolation, } from './testing.js';
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAKH,OAAO,wBAAwB,CAAC;AAKhC,OAAO,EACL,KAAK,iBAAiB,EAEtB,gBAAgB,EAEhB,uBAAuB,EAEvB,qBAAqB,EACrB,KAAK,wBAAwB,EAC7B,KAAK,sBAAsB,GAC5B,MAAM,qBAAqB,CAAC;AAI7B,OAAO,EACL,kBAAkB,EAElB,gBAAgB,EAChB,WAAW,EACX,gBAAgB,EAChB,kBAAkB,EAElB,eAAe,EACf,KAAK,oBAAoB,EACzB,aAAa,EACb,eAAe,EAEf,aAAa,EAEb,KAAK,iBAAiB,EAEtB,kBAAkB,EAClB,oBAAoB,EACpB,oBAAoB,EACpB,iBAAiB,EAEjB,UAAU,EACV,cAAc,GACf,MAAM,cAAc,CAAC;AAKtB,OAAO,EAEL,YAAY,EACZ,KAAK,mBAAmB,EAExB,QAAQ,GACT,MAAM,iBAAiB,CAAC;AAIzB,OAAO,EACL,yBAAyB,EACzB,KAAK,uBAAuB,GAC7B,MAAM,kBAAkB,CAAC;AAI1B,OAAO,EACL,uBAAuB,EACvB,eAAe,EACf,KAAK,uBAAuB,EAC5B,KAAK,oBAAoB,GAC1B,MAAM,aAAa,CAAC;AAIrB,OAAO,EAEL,uBAAuB,EACvB,cAAc,EAEd,aAAa,EACb,gBAAgB,EAChB,KAAK,cAAc,EAEnB,KAAK,wBAAwB,GAC9B,MAAM,kBAAkB,CAAC;AAI1B,OAAO,EACL,yBAAyB,EACzB,yBAAyB,EACzB,qBAAqB,EACrB,mBAAmB,EACnB,yBAAyB,EACzB,KAAK,kBAAkB,EACvB,2BAA2B,GAC5B,MAAM,eAAe,CAAC;AAKvB,OAAO,EACL,2BAA2B,EAC3B,8BAA8B,EAC9B,uBAAuB,EACvB,YAAY,EACZ,KAAK,uBAAuB,EAC5B,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,cAAc,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { ObjectRegistry, applyPendingDecoratorRegistrations, registerCompatibleFieldDecorator } from "@happyvertical/smrt-core";
|
|
2
|
+
import { c, a, b } from "./chunks/sveltekit-9eRH1RLw.js";
|
|
3
|
+
import { T, a as a2, b as b2, e, g, c as c2, h, i, d, r, f, w, j, k, l } from "./chunks/context-B5CKsmMi.js";
|
|
4
|
+
import { r as registerTenantScopedClass } from "./chunks/testing-C_tV23JW.js";
|
|
5
|
+
import { a as a3, b as b3, c as c3, d as d2, e as e2, f as f2, g as g2, h as h2, i as i2, j as j2, k as k2, l as l2, m, s, t, u } from "./chunks/testing-C_tV23JW.js";
|
|
6
|
+
ObjectRegistry.registerPackageManifest(
|
|
7
|
+
new URL("./manifest.json", import.meta.url)
|
|
8
|
+
);
|
|
9
|
+
function TenantScoped(options = {}) {
|
|
10
|
+
return (target, decoratorContext) => {
|
|
11
|
+
applyPendingDecoratorRegistrations(target, decoratorContext);
|
|
12
|
+
const className = target.name;
|
|
13
|
+
const config = {
|
|
14
|
+
mode: options.mode ?? "required",
|
|
15
|
+
field: options.field ?? "tenantId",
|
|
16
|
+
autoFilter: options.autoFilter ?? true,
|
|
17
|
+
autoPopulate: options.autoPopulate ?? true,
|
|
18
|
+
allowSuperAdminBypass: options.allowSuperAdminBypass ?? false
|
|
19
|
+
};
|
|
20
|
+
registerTenantScopedClass(className, config);
|
|
21
|
+
return target;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function tenantId(options = {}) {
|
|
25
|
+
const opts = {
|
|
26
|
+
autoFilter: true,
|
|
27
|
+
required: true,
|
|
28
|
+
autoPopulate: true,
|
|
29
|
+
nullable: false,
|
|
30
|
+
...options
|
|
31
|
+
};
|
|
32
|
+
return ((targetOrValue, propertyKeyOrContext) => {
|
|
33
|
+
registerCompatibleFieldDecorator(
|
|
34
|
+
targetOrValue,
|
|
35
|
+
propertyKeyOrContext,
|
|
36
|
+
(className, propertyKey) => {
|
|
37
|
+
ObjectRegistry.registerFieldDecorator(className, propertyKey, {
|
|
38
|
+
type: "foreignKey",
|
|
39
|
+
related: "Tenant",
|
|
40
|
+
sqlType: "UUID",
|
|
41
|
+
required: opts.required,
|
|
42
|
+
nullable: opts.nullable,
|
|
43
|
+
__tenancy: {
|
|
44
|
+
...opts,
|
|
45
|
+
isTenantIdField: true
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function isTenantIdField(field) {
|
|
53
|
+
if (!field || typeof field !== "object") {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
const def = field;
|
|
57
|
+
const tenancy = def.__tenancy;
|
|
58
|
+
return tenancy?.isTenantIdField === true;
|
|
59
|
+
}
|
|
60
|
+
function getTenantIdFieldOptions(field) {
|
|
61
|
+
if (!isTenantIdField(field)) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const def = field;
|
|
65
|
+
return def.__tenancy;
|
|
66
|
+
}
|
|
67
|
+
export {
|
|
68
|
+
T as TenantContext,
|
|
69
|
+
a2 as TenantContextError,
|
|
70
|
+
b2 as TenantIsolationError,
|
|
71
|
+
TenantScoped,
|
|
72
|
+
a3 as assertTenantContextRequired,
|
|
73
|
+
b3 as assertTenantIsolationViolation,
|
|
74
|
+
c3 as clearTenantScopedRegistry,
|
|
75
|
+
c as createCliContext,
|
|
76
|
+
a as createExpressMiddleware,
|
|
77
|
+
b as createSvelteKitHandle,
|
|
78
|
+
d2 as createTenantInterceptor,
|
|
79
|
+
e2 as createTestTenantContext,
|
|
80
|
+
f2 as disableTenancy,
|
|
81
|
+
g2 as enableTenancy,
|
|
82
|
+
e as enterTenantContext,
|
|
83
|
+
h2 as getAllTenantScopedClasses,
|
|
84
|
+
g as getCurrentTenant,
|
|
85
|
+
c2 as getTenantId,
|
|
86
|
+
getTenantIdFieldOptions,
|
|
87
|
+
i2 as getTenantScopedConfig,
|
|
88
|
+
h as hasTenantContext,
|
|
89
|
+
i as isSuperAdminBypass,
|
|
90
|
+
d as isSystemContext,
|
|
91
|
+
j2 as isTenancyEnabled,
|
|
92
|
+
isTenantIdField,
|
|
93
|
+
k2 as isTenantScopedClass,
|
|
94
|
+
registerTenantScopedClass,
|
|
95
|
+
r as requireTenant,
|
|
96
|
+
f as requireTenantId,
|
|
97
|
+
l2 as resetTenancy,
|
|
98
|
+
m as runTenantScopedEntryPoint,
|
|
99
|
+
s as setupTestTenancy,
|
|
100
|
+
tenantId,
|
|
101
|
+
t as testTenantIsolation,
|
|
102
|
+
u as unregisterTenantScopedClass,
|
|
103
|
+
w as withSuperAdminBypass,
|
|
104
|
+
j as withSystemContext,
|
|
105
|
+
k as withTenant,
|
|
106
|
+
l as withTenantSync
|
|
107
|
+
};
|
|
108
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/__smrt-register__.ts","../src/decorators.ts","../src/fields.ts"],"sourcesContent":["/**\n * Self-registers this package's build-time manifest before any @smrt() decorator\n * in the package fires. Fixes issue #1132: in consumer runtimes (tsx, SvelteKit\n * SSR, plain `vite dev`) the decorator's synchronous manifest lookup previously\n * missed because no step populated the global manifest cache — classes got\n * registered with zero fields and `save()` / `toJSON()` silently dropped every\n * declared property.\n *\n * Import this module as the first statement in `src/index.ts` so its top-level\n * side effect runs ahead of any class module's @smrt() decorator.\n *\n * Silent no-op in dev/test, where the vitest plugin already populates manifests\n * via a different path. Only needs to succeed in the published dist output.\n *\n * @see https://github.com/happyvertical/smrt/issues/1132\n */\nimport { ObjectRegistry } from '@happyvertical/smrt-core';\n\n// `new URL('./manifest.json', import.meta.url)` resolves at runtime to the\n// manifest sitting next to this module's compiled output. Vite warns at build\n// time that it cannot pre-resolve the URL; that is the intended behavior —\n// the URL must resolve to dist/manifest.json at runtime, not be inlined.\nObjectRegistry.registerPackageManifest(\n new URL('./manifest.json', import.meta.url),\n);\n","/**\n * Tenancy Decorators\n *\n * Provides class and property decorators for tenant-scoped SMRT objects.\n *\n * @example\n * ```typescript\n * import { smrt, SmrtObject } from '@happyvertical/smrt-core';\n * import { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\n *\n * @smrt()\n * @TenantScoped({ mode: 'optional' })\n * class Document extends SmrtObject {\n * @tenantId({ nullable: true })\n * tenantId: string | null = null; // null = global document\n *\n * title: string = '';\n * }\n * ```\n *\n * @see https://github.com/happyvertical/smrt/issues/675\n * @see https://github.com/happyvertical/smrt/issues/829\n */\n\nimport {\n applyPendingDecoratorRegistrations,\n type CompatiblePropertyDecorator,\n type CompatiblePropertyDecoratorContext,\n type LegacyPropertyDecoratorTarget,\n ObjectRegistry,\n registerCompatibleFieldDecorator,\n} from '@happyvertical/smrt-core';\nimport type { TenantIdFieldOptions } from './fields.js';\nimport {\n registerTenantScopedClass,\n type TenantScopedConfig,\n} from './registry.js';\n\n/**\n * Options accepted by the `@TenantScoped()` class decorator.\n *\n * All fields are optional; defaults match the most restrictive safe behaviour\n * (required mode, auto-filter and auto-populate enabled, no super-admin bypass).\n *\n * @see TenantScoped\n * @see TenantScopedConfig\n */\nexport interface TenantScopedOptions {\n /**\n * Tenancy mode for this class\n * - 'required': Must have tenant context for all operations (default)\n * - 'optional': Works with or without tenant context\n */\n mode?: 'required' | 'optional';\n\n /**\n * Field name containing tenant ID\n * @default 'tenantId'\n */\n field?: string;\n\n /**\n * Auto-filter all queries by tenant\n * @default true\n */\n autoFilter?: boolean;\n\n /**\n * Auto-populate tenant ID from context on create\n * @default true\n */\n autoPopulate?: boolean;\n\n /**\n * Allow super admin bypass for this class\n * @default false - must be explicitly enabled\n */\n allowSuperAdminBypass?: boolean;\n}\n\n/**\n * Mark a class as tenant-scoped\n *\n * This decorator registers the class with the tenancy system so that:\n * - list()/get() queries are automatically filtered by tenant\n * - save() validates tenant ID matches current context\n * - delete() validates tenant ownership\n * - Raw SQL queries trigger policy enforcement\n *\n * @param options - Configuration options\n *\n * @example Basic usage (required tenancy)\n * ```typescript\n * @smrt()\n * @TenantScoped()\n * class Document extends SmrtObject {\n * @tenantId()\n * tenantId: string = '';\n *\n * title: string = '';\n * }\n * ```\n *\n * @example With super admin bypass enabled\n * ```typescript\n * @smrt()\n * @TenantScoped({ allowSuperAdminBypass: true })\n * class AuditLog extends SmrtObject {\n * @tenantId()\n * tenantId: string = '';\n *\n * action: string = '';\n * }\n * ```\n *\n * @example Optional tenancy (works with or without context)\n * ```typescript\n * @smrt()\n * @TenantScoped({ mode: 'optional' })\n * class GlobalConfig extends SmrtObject {\n * @tenantId({ nullable: true })\n * tenantId: string | null = null; // null = global, string = tenant-specific\n *\n * key: string = '';\n * value: string = '';\n * }\n * ```\n */\nexport function TenantScoped(options: TenantScopedOptions = {}) {\n return <T extends Function>(\n target: T,\n decoratorContext?: ClassDecoratorContext,\n ): T => {\n applyPendingDecoratorRegistrations(target, decoratorContext);\n\n const className = target.name;\n\n // Merge with defaults\n const config: Partial<TenantScopedConfig> = {\n mode: options.mode ?? 'required',\n field: options.field ?? 'tenantId',\n autoFilter: options.autoFilter ?? true,\n autoPopulate: options.autoPopulate ?? true,\n allowSuperAdminBypass: options.allowSuperAdminBypass ?? false,\n };\n\n // Register with the tenancy system\n registerTenantScopedClass(className, config);\n\n // Return the class unchanged\n return target;\n };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Property Decorator: @tenantId\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Tenant ID property decorator\n *\n * Marks a property as the tenant identifier field. This decorator registers\n * the field metadata with ObjectRegistry, keeping the property value clean\n * (no descriptor objects that could be accidentally saved to the database).\n *\n * @param options - Field options (nullable, autoFilter, autoPopulate, etc.)\n * @returns Property decorator\n *\n * @example Basic usage (required tenancy)\n * ```typescript\n * @smrt()\n * @TenantScoped()\n * class Document extends SmrtObject {\n * @tenantId()\n * tenantId: string = '';\n *\n * title: string = '';\n * }\n * ```\n *\n * @example Nullable tenant ID (for global resources)\n * ```typescript\n * @smrt()\n * @TenantScoped({ mode: 'optional' })\n * class GlobalConfig extends SmrtObject {\n * @tenantId({ nullable: true })\n * tenantId: string | null = null; // null = global, string = tenant-specific\n *\n * key: string = '';\n * }\n * ```\n *\n * @see https://github.com/happyvertical/smrt/issues/829 - Why decorators over field helpers\n */\nexport function tenantId(options: TenantIdFieldOptions = {}) {\n const opts = {\n autoFilter: true,\n required: true,\n autoPopulate: true,\n nullable: false,\n ...options,\n };\n\n return ((\n targetOrValue: LegacyPropertyDecoratorTarget | undefined,\n propertyKeyOrContext: CompatiblePropertyDecoratorContext<any, any>,\n ) => {\n registerCompatibleFieldDecorator(\n targetOrValue,\n propertyKeyOrContext,\n (className, propertyKey) => {\n ObjectRegistry.registerFieldDecorator(className, propertyKey, {\n type: 'foreignKey',\n related: 'Tenant',\n sqlType: 'UUID',\n required: opts.required,\n nullable: opts.nullable,\n __tenancy: {\n ...opts,\n isTenantIdField: true,\n },\n });\n },\n );\n }) as CompatiblePropertyDecorator;\n}\n","/**\n * Tenancy Field Types and Utilities\n *\n * This module provides types and utility functions for tenant ID fields.\n * The actual field decorator is in decorators.ts.\n *\n * @see https://github.com/happyvertical/smrt/issues/675\n * @see https://github.com/happyvertical/smrt/issues/829\n */\n\n/**\n * Options for the `@tenantId()` property decorator.\n *\n * Controls how the decorated field interacts with the tenancy interceptor.\n * All options default to the strictest safe values: auto-filter on, required,\n * auto-populate on, not nullable.\n *\n * @see tenantId\n * @see TenantScopedOptions\n */\nexport interface TenantIdFieldOptions {\n /**\n * Auto-filter queries by this field\n * @default true\n */\n autoFilter?: boolean;\n\n /**\n * Require this field to have a value on save\n * @default true\n */\n required?: boolean;\n\n /**\n * Auto-populate from context on create if not set\n * @default true\n */\n autoPopulate?: boolean;\n\n /**\n * Allow null values (for global resources)\n * @default false\n */\n nullable?: boolean;\n}\n\n// Symbol to identify tenantId fields\nexport const TENANT_ID_SYMBOL = Symbol('tenantId');\n\n/**\n * Internal field descriptor stored in `ObjectRegistry` when `@tenantId()` is\n * applied to a property.\n *\n * Consumers should use `isTenantIdField()` and `getTenantIdFieldOptions()`\n * to inspect these descriptors rather than reading the raw properties directly.\n *\n * @see isTenantIdField\n * @see getTenantIdFieldOptions\n */\nexport interface TenantIdFieldDefinition {\n /** Field type marker */\n type: 'foreignKey';\n /** Reference to Tenant class (placeholder - actual class resolved at runtime) */\n reference: 'Tenant';\n /** SQL type */\n sqlType: 'UUID';\n /** Field is required */\n required: boolean;\n /** Field allows null */\n nullable: boolean;\n /** Tenancy-specific options */\n __tenancy: TenantIdFieldOptions & { isTenantIdField: true };\n}\n\n/**\n * Return `true` if the given field definition was produced by the `@tenantId()`\n * decorator (i.e., it has an `__tenancy.isTenantIdField` marker).\n *\n * Used internally by the interceptor and code generators to locate the tenant\n * ID field on a class without knowing its property name in advance.\n *\n * @param field - A raw field definition object, typically from `ObjectRegistry`.\n * @returns `true` if `field` is a tenant ID field definition, `false` otherwise.\n *\n * @example\n * ```typescript\n * const fields = ObjectRegistry.getFields('Document');\n * const tenantField = Object.entries(fields).find(([, def]) => isTenantIdField(def));\n * ```\n *\n * @see getTenantIdFieldOptions\n * @see TenantIdFieldDefinition\n */\nexport function isTenantIdField(field: unknown): boolean {\n if (!field || typeof field !== 'object') {\n return false;\n }\n const def = field as Record<string, unknown>;\n const tenancy = def.__tenancy as Record<string, unknown> | undefined;\n return tenancy?.isTenantIdField === true;\n}\n\n/**\n * Extract the `TenantIdFieldOptions` from a field definition.\n *\n * Returns the tenancy-specific options (autoFilter, required, autoPopulate,\n * nullable) stored inside the field descriptor's `__tenancy` property.\n * Returns `null` if the field was not produced by `@tenantId()`.\n *\n * @param field - A raw field definition object, typically from `ObjectRegistry`.\n * @returns The `TenantIdFieldOptions` if the field is a tenant ID field,\n * `null` otherwise.\n *\n * @see isTenantIdField\n * @see TenantIdFieldOptions\n */\nexport function getTenantIdFieldOptions(\n field: unknown,\n): TenantIdFieldOptions | null {\n if (!isTenantIdField(field)) {\n return null;\n }\n const def = field as { __tenancy: TenantIdFieldOptions };\n return def.__tenancy;\n}\n"],"names":[],"mappings":";;;;;AAsBA,eAAe;AAAA,EACb,IAAA,IAAA,mBAAA,YAAA,GAAA;AACF;ACwGO,SAAS,aAAa,UAA+B,IAAI;AAC9D,SAAO,CACL,QACA,qBACM;AACN,uCAAmC,QAAQ,gBAAgB;AAE3D,UAAM,YAAY,OAAO;AAGzB,UAAM,SAAsC;AAAA,MAC1C,MAAM,QAAQ,QAAQ;AAAA,MACtB,OAAO,QAAQ,SAAS;AAAA,MACxB,YAAY,QAAQ,cAAc;AAAA,MAClC,cAAc,QAAQ,gBAAgB;AAAA,MACtC,uBAAuB,QAAQ,yBAAyB;AAAA,IAAA;AAI1D,8BAA0B,WAAW,MAAM;AAG3C,WAAO;AAAA,EACT;AACF;AA0CO,SAAS,SAAS,UAAgC,IAAI;AAC3D,QAAM,OAAO;AAAA,IACX,YAAY;AAAA,IACZ,UAAU;AAAA,IACV,cAAc;AAAA,IACd,UAAU;AAAA,IACV,GAAG;AAAA,EAAA;AAGL,UAAQ,CACN,eACA,yBACG;AACH;AAAA,MACE;AAAA,MACA;AAAA,MACA,CAAC,WAAW,gBAAgB;AAC1B,uBAAe,uBAAuB,WAAW,aAAa;AAAA,UAC5D,MAAM;AAAA,UACN,SAAS;AAAA,UACT,SAAS;AAAA,UACT,UAAU,KAAK;AAAA,UACf,UAAU,KAAK;AAAA,UACf,WAAW;AAAA,YACT,GAAG;AAAA,YACH,iBAAiB;AAAA,UAAA;AAAA,QACnB,CACD;AAAA,MACH;AAAA,IAAA;AAAA,EAEJ;AACF;ACpIO,SAAS,gBAAgB,OAAyB;AACvD,MAAI,CAAC,SAAS,OAAO,UAAU,UAAU;AACvC,WAAO;AAAA,EACT;AACA,QAAM,MAAM;AACZ,QAAM,UAAU,IAAI;AACpB,SAAO,SAAS,oBAAoB;AACtC;AAgBO,SAAS,wBACd,OAC6B;AAC7B,MAAI,CAAC,gBAAgB,KAAK,GAAG;AAC3B,WAAO;AAAA,EACT;AACA,QAAM,MAAM;AACZ,SAAO,IAAI;AACb;"}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { CollectionInterceptor, DispatchBus, InterceptorContext } from '@happyvertical/smrt-core';
|
|
2
|
+
import { isTenancyEnabled } from './enabled-state.js';
|
|
3
|
+
/**
|
|
4
|
+
* Policy controlling what happens when raw SQL is executed against a
|
|
5
|
+
* tenant-scoped class without an explicit bypass.
|
|
6
|
+
*
|
|
7
|
+
* - `'throw'` — Raises a `TenantIsolationError` (most secure; default).
|
|
8
|
+
* - `'warn'` — Logs a `console.warn` but allows the query to proceed (useful
|
|
9
|
+
* during migration periods).
|
|
10
|
+
* - `'allow'` — Silently allows the query; not recommended for production.
|
|
11
|
+
*
|
|
12
|
+
* @see TenantInterceptorOptions.rawQueryPolicy
|
|
13
|
+
* @see enableTenancy
|
|
14
|
+
*/
|
|
15
|
+
export type RawQueryPolicy = 'throw' | 'warn' | 'allow';
|
|
16
|
+
/**
|
|
17
|
+
* Configuration options accepted by `createTenantInterceptor()` and
|
|
18
|
+
* `enableTenancy()`.
|
|
19
|
+
*
|
|
20
|
+
* All options are optional; reasonable defaults are applied. The callback
|
|
21
|
+
* hooks (`onRawQuery`, `onMissingContext`, `onIsolationViolation`) are useful
|
|
22
|
+
* for logging and alerting without altering the enforcement behaviour.
|
|
23
|
+
*
|
|
24
|
+
* @see createTenantInterceptor
|
|
25
|
+
* @see enableTenancy
|
|
26
|
+
*/
|
|
27
|
+
export interface TenantInterceptorOptions {
|
|
28
|
+
/**
|
|
29
|
+
* Policy for raw SQL queries on tenant-scoped classes
|
|
30
|
+
* - 'throw': Throw error (most secure, default)
|
|
31
|
+
* - 'warn': Log warning but allow (for migration)
|
|
32
|
+
* - 'allow': Silently allow (not recommended for production)
|
|
33
|
+
* @default 'throw'
|
|
34
|
+
*/
|
|
35
|
+
rawQueryPolicy?: RawQueryPolicy;
|
|
36
|
+
/**
|
|
37
|
+
* Called when a raw query is attempted on a tenant-scoped class
|
|
38
|
+
* Useful for logging/auditing
|
|
39
|
+
*/
|
|
40
|
+
onRawQuery?: (className: string, sql: string, context: InterceptorContext) => void;
|
|
41
|
+
/**
|
|
42
|
+
* Called when tenant context is missing for a tenant-scoped operation
|
|
43
|
+
*/
|
|
44
|
+
onMissingContext?: (className: string, operation: string, context: InterceptorContext) => void;
|
|
45
|
+
/**
|
|
46
|
+
* Called when an isolation violation is detected
|
|
47
|
+
*/
|
|
48
|
+
onIsolationViolation?: (className: string, expectedTenantId: string, actualTenantId: string, context: InterceptorContext) => void;
|
|
49
|
+
/**
|
|
50
|
+
* DispatchBus instance for emitting provisioning events on lifecycle changes.
|
|
51
|
+
* When provided along with directoryClasses, afterSave/afterDelete hooks
|
|
52
|
+
* emit dispatches like `directory.membership.created`.
|
|
53
|
+
*/
|
|
54
|
+
dispatchBus?: DispatchBus;
|
|
55
|
+
/**
|
|
56
|
+
* Class names to emit directory dispatches for on save/delete lifecycle events.
|
|
57
|
+
* Only classes listed here will trigger dispatch emissions.
|
|
58
|
+
* @example ['Tenant', 'Membership', 'User']
|
|
59
|
+
*/
|
|
60
|
+
directoryClasses?: string[];
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Create a `CollectionInterceptor` that enforces tenant isolation on all
|
|
64
|
+
* `SmrtCollection` operations.
|
|
65
|
+
*
|
|
66
|
+
* The returned interceptor hooks into the smrt-core `GlobalInterceptors`
|
|
67
|
+
* pipeline at priority 100 (runs before all other interceptors) and
|
|
68
|
+
* handles the following lifecycle hooks:
|
|
69
|
+
*
|
|
70
|
+
* | Hook | Behaviour |
|
|
71
|
+
* |---------------|-----------|
|
|
72
|
+
* | `beforeList` | Injects tenant filter into `WHERE`; validates explicit filters. |
|
|
73
|
+
* | `beforeGet` | Converts ID lookups to `{ id, tenantId }` filter objects. |
|
|
74
|
+
* | `beforeSave` | Auto-populates `tenantId`; validates existing values. |
|
|
75
|
+
* | `beforeDelete`| Validates the instance's `tenantId` matches context. |
|
|
76
|
+
* | `beforeQuery` | Enforces `rawQueryPolicy` on raw SQL calls. |
|
|
77
|
+
* | `afterSave` | Emits `directory.<class>.created/updated` via `dispatchBus`. |
|
|
78
|
+
* | `afterDelete` | Emits `directory.<class>.deleted` via `dispatchBus`. |
|
|
79
|
+
*
|
|
80
|
+
* Use `enableTenancy()` to register the interceptor globally. Call this
|
|
81
|
+
* directly only when you need multiple interceptor instances (e.g., for
|
|
82
|
+
* isolated tests or feature flags).
|
|
83
|
+
*
|
|
84
|
+
* @param options - Configuration for the interceptor.
|
|
85
|
+
* @returns A `CollectionInterceptor` ready to be registered with
|
|
86
|
+
* `GlobalInterceptors.register()`.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```typescript
|
|
90
|
+
* import { createTenantInterceptor } from '@happyvertical/smrt-tenancy';
|
|
91
|
+
* import { GlobalInterceptors } from '@happyvertical/smrt-core';
|
|
92
|
+
*
|
|
93
|
+
* const interceptor = createTenantInterceptor({ rawQueryPolicy: 'warn' });
|
|
94
|
+
* GlobalInterceptors.register(interceptor);
|
|
95
|
+
* ```
|
|
96
|
+
*
|
|
97
|
+
* @see enableTenancy
|
|
98
|
+
* @see TenantInterceptorOptions
|
|
99
|
+
*/
|
|
100
|
+
export declare function createTenantInterceptor(options?: TenantInterceptorOptions): CollectionInterceptor;
|
|
101
|
+
/**
|
|
102
|
+
* Enable tenant enforcement globally
|
|
103
|
+
*
|
|
104
|
+
* Call this once at application startup to enable automatic tenant isolation.
|
|
105
|
+
*
|
|
106
|
+
* @param options - Configuration options
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* ```typescript
|
|
110
|
+
* // In your app initialization
|
|
111
|
+
* import { enableTenancy } from '@happyvertical/smrt-tenancy';
|
|
112
|
+
*
|
|
113
|
+
* enableTenancy({
|
|
114
|
+
* rawQueryPolicy: 'throw',
|
|
115
|
+
* onMissingContext: (className, operation) => {
|
|
116
|
+
* console.error(`Missing tenant context for ${operation} on ${className}`);
|
|
117
|
+
* }
|
|
118
|
+
* });
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
export declare function enableTenancy(options?: TenantInterceptorOptions): void;
|
|
122
|
+
/**
|
|
123
|
+
* Disable global tenant enforcement.
|
|
124
|
+
*
|
|
125
|
+
* Unregisters the interceptor previously installed by `enableTenancy()` and
|
|
126
|
+
* resets the internal enabled flag so `enableTenancy()` can be called again.
|
|
127
|
+
* Idempotent — safe to call even when tenancy was never enabled.
|
|
128
|
+
*
|
|
129
|
+
* Common use-cases:
|
|
130
|
+
* - Test teardown (via `resetTenancy()`).
|
|
131
|
+
* - Temporarily disabling tenancy before reconfiguring with new options.
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```typescript
|
|
135
|
+
* afterAll(() => {
|
|
136
|
+
* disableTenancy();
|
|
137
|
+
* });
|
|
138
|
+
* ```
|
|
139
|
+
*
|
|
140
|
+
* @see enableTenancy
|
|
141
|
+
* @see isTenancyEnabled
|
|
142
|
+
* @see resetTenancy
|
|
143
|
+
*/
|
|
144
|
+
export declare function disableTenancy(): void;
|
|
145
|
+
/**
|
|
146
|
+
* Return `true` if tenant enforcement is currently active.
|
|
147
|
+
*
|
|
148
|
+
* Reflects whether `enableTenancy()` has been called and the interceptor has not
|
|
149
|
+
* yet been removed by `disableTenancy()`. Re-exported from `enabled-state.ts`
|
|
150
|
+
* (the shared leaf module) so the public API surface is unchanged.
|
|
151
|
+
*
|
|
152
|
+
* @see enableTenancy
|
|
153
|
+
* @see disableTenancy
|
|
154
|
+
*/
|
|
155
|
+
export { isTenancyEnabled };
|
|
156
|
+
//# sourceMappingURL=interceptor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"interceptor.d.ts","sourceRoot":"","sources":["../src/interceptor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,WAAW,EAEhB,KAAK,kBAAkB,EAMxB,MAAM,0BAA0B,CAAC;AASlC,OAAO,EAAE,gBAAgB,EAAqB,MAAM,oBAAoB,CAAC;AAMzE;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC;AAExD;;;;;;;;;;GAUG;AACH,MAAM,WAAW,wBAAwB;IACvC;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,cAAc,CAAC;IAEhC;;;OAGG;IACH,UAAU,CAAC,EAAE,CACX,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,kBAAkB,KACxB,IAAI,CAAC;IAEV;;OAEG;IACH,gBAAgB,CAAC,EAAE,CACjB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,kBAAkB,KACxB,IAAI,CAAC;IAEV;;OAEG;IACH,oBAAoB,CAAC,EAAE,CACrB,SAAS,EAAE,MAAM,EACjB,gBAAgB,EAAE,MAAM,EACxB,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,kBAAkB,KACxB,IAAI,CAAC;IAEV;;;;OAIG;IACH,WAAW,CAAC,EAAE,WAAW,CAAC;IAE1B;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC7B;AA2CD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,GAAE,wBAA6B,GACrC,qBAAqB,CA6ZvB;AAWD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,aAAa,CAAC,OAAO,GAAE,wBAA6B,GAAG,IAAI,CAuB1E;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,cAAc,IAAI,IAAI,CAarC;AAED;;;;;;;;;GASG;AACH,OAAO,EAAE,gBAAgB,EAAE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"playground.d.ts","sourceRoot":"","sources":["../src/playground.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { TENANCY_MODULE_META } from "./ui.js";
|
|
2
|
+
const noop = () => {
|
|
3
|
+
};
|
|
4
|
+
const sampleTenants = [
|
|
5
|
+
{
|
|
6
|
+
id: "tenant-riverstone",
|
|
7
|
+
name: "Riverstone Newsroom",
|
|
8
|
+
slug: "riverstone-newsroom",
|
|
9
|
+
status: "active"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
id: "tenant-harbor",
|
|
13
|
+
name: "Harbor Labs",
|
|
14
|
+
slug: "harbor-labs",
|
|
15
|
+
status: "suspended"
|
|
16
|
+
}
|
|
17
|
+
];
|
|
18
|
+
const sampleMemberships = [
|
|
19
|
+
{
|
|
20
|
+
id: "membership-riverstone",
|
|
21
|
+
tenantId: "tenant-riverstone"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: "membership-harbor",
|
|
25
|
+
tenantId: "tenant-harbor"
|
|
26
|
+
}
|
|
27
|
+
];
|
|
28
|
+
const loadTenantCard = () => import("./svelte/components/TenantCard.svelte");
|
|
29
|
+
const loadTenantSwitcher = () => import("./svelte/components/TenantSwitcher.svelte");
|
|
30
|
+
const playground = {
|
|
31
|
+
packageName: "@happyvertical/smrt-tenancy",
|
|
32
|
+
displayName: TENANCY_MODULE_META.displayName,
|
|
33
|
+
description: TENANCY_MODULE_META.description,
|
|
34
|
+
moduleMeta: TENANCY_MODULE_META,
|
|
35
|
+
entries: [
|
|
36
|
+
{
|
|
37
|
+
id: "tenant-card",
|
|
38
|
+
title: "Tenant Card",
|
|
39
|
+
description: "Tenant identity and status card with optional membership count and actions.",
|
|
40
|
+
loadComponent: loadTenantCard,
|
|
41
|
+
order: 1,
|
|
42
|
+
props: {
|
|
43
|
+
tenant: sampleTenants[0],
|
|
44
|
+
memberCount: 18,
|
|
45
|
+
selected: true,
|
|
46
|
+
actions: true,
|
|
47
|
+
onclick: noop,
|
|
48
|
+
onedit: noop,
|
|
49
|
+
ondelete: noop
|
|
50
|
+
},
|
|
51
|
+
modes: {
|
|
52
|
+
mock: {
|
|
53
|
+
label: "Mock"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "tenant-switcher",
|
|
59
|
+
title: "Tenant Switcher",
|
|
60
|
+
description: "Membership-aware tenant selector used for multi-tenant application shells.",
|
|
61
|
+
loadComponent: loadTenantSwitcher,
|
|
62
|
+
order: 2,
|
|
63
|
+
props: {
|
|
64
|
+
memberships: sampleMemberships,
|
|
65
|
+
tenants: new Map(sampleTenants.map((tenant) => [tenant.id, tenant])),
|
|
66
|
+
currentTenantId: "tenant-riverstone",
|
|
67
|
+
onchange: noop
|
|
68
|
+
},
|
|
69
|
+
modes: {
|
|
70
|
+
mock: {
|
|
71
|
+
label: "Mock"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
};
|
|
77
|
+
export {
|
|
78
|
+
playground as default
|
|
79
|
+
};
|
|
80
|
+
//# sourceMappingURL=playground.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"playground.js","sources":["../src/svelte/playground.ts"],"sourcesContent":["import { TENANCY_MODULE_META } from '../ui.js';\n\nconst noop = () => {};\n\nconst sampleTenants = [\n {\n id: 'tenant-riverstone',\n name: 'Riverstone Newsroom',\n slug: 'riverstone-newsroom',\n status: 'active',\n },\n {\n id: 'tenant-harbor',\n name: 'Harbor Labs',\n slug: 'harbor-labs',\n status: 'suspended',\n },\n];\n\nconst sampleMemberships = [\n {\n id: 'membership-riverstone',\n tenantId: 'tenant-riverstone',\n },\n {\n id: 'membership-harbor',\n tenantId: 'tenant-harbor',\n },\n];\n\nconst loadTenantCard = () => import('./components/TenantCard.svelte');\nconst loadTenantSwitcher = () => import('./components/TenantSwitcher.svelte');\n\nexport default {\n packageName: '@happyvertical/smrt-tenancy',\n displayName: TENANCY_MODULE_META.displayName,\n description: TENANCY_MODULE_META.description,\n moduleMeta: TENANCY_MODULE_META,\n entries: [\n {\n id: 'tenant-card',\n title: 'Tenant Card',\n description:\n 'Tenant identity and status card with optional membership count and actions.',\n loadComponent: loadTenantCard,\n order: 1,\n props: {\n tenant: sampleTenants[0],\n memberCount: 18,\n selected: true,\n actions: true,\n onclick: noop,\n onedit: noop,\n ondelete: noop,\n },\n modes: {\n mock: {\n label: 'Mock',\n },\n },\n },\n {\n id: 'tenant-switcher',\n title: 'Tenant Switcher',\n description:\n 'Membership-aware tenant selector used for multi-tenant application shells.',\n loadComponent: loadTenantSwitcher,\n order: 2,\n props: {\n memberships: sampleMemberships,\n tenants: new Map(sampleTenants.map((tenant) => [tenant.id, tenant])),\n currentTenantId: 'tenant-riverstone',\n onchange: noop,\n },\n modes: {\n mock: {\n label: 'Mock',\n },\n },\n },\n ],\n};\n"],"names":[],"mappings":";AAEA,MAAM,OAAO,MAAM;AAAC;AAEpB,MAAM,gBAAgB;AAAA,EACpB;AAAA,IACE,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,IACN,QAAQ;AAAA,EAAA;AAAA,EAEV;AAAA,IACE,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,IACN,QAAQ;AAAA,EAAA;AAEZ;AAEA,MAAM,oBAAoB;AAAA,EACxB;AAAA,IACE,IAAI;AAAA,IACJ,UAAU;AAAA,EAAA;AAAA,EAEZ;AAAA,IACE,IAAI;AAAA,IACJ,UAAU;AAAA,EAAA;AAEd;AAEA,MAAM,iBAAiB,MAAM,OAAO,uCAAgC;AACpE,MAAM,qBAAqB,MAAM,OAAO,2CAAoC;AAE5E,MAAA,aAAe;AAAA,EACb,aAAa;AAAA,EACb,aAAa,oBAAoB;AAAA,EACjC,aAAa,oBAAoB;AAAA,EACjC,YAAY;AAAA,EACZ,SAAS;AAAA,IACP;AAAA,MACE,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,aACE;AAAA,MACF,eAAe;AAAA,MACf,OAAO;AAAA,MACP,OAAO;AAAA,QACL,QAAQ,cAAc,CAAC;AAAA,QACvB,aAAa;AAAA,QACb,UAAU;AAAA,QACV,SAAS;AAAA,QACT,SAAS;AAAA,QACT,QAAQ;AAAA,QACR,UAAU;AAAA,MAAA;AAAA,MAEZ,OAAO;AAAA,QACL,MAAM;AAAA,UACJ,OAAO;AAAA,QAAA;AAAA,MACT;AAAA,IACF;AAAA,IAEF;AAAA,MACE,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,aACE;AAAA,MACF,eAAe;AAAA,MACf,OAAO;AAAA,MACP,OAAO;AAAA,QACL,aAAa;AAAA,QACb,SAAS,IAAI,IAAI,cAAc,IAAI,CAAC,WAAW,CAAC,OAAO,IAAI,MAAM,CAAC,CAAC;AAAA,QACnE,iBAAiB;AAAA,QACjB,UAAU;AAAA,MAAA;AAAA,MAEZ,OAAO;AAAA,QACL,MAAM;AAAA,UACJ,OAAO;AAAA,QAAA;AAAA,MACT;AAAA,IACF;AAAA,EACF;AAEJ;"}
|