@flusys/nestjs-form-builder 6.0.1 → 6.1.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.
@@ -0,0 +1,129 @@
1
+ function _define_property(obj, key, value) {
2
+ if (key in obj) {
3
+ Object.defineProperty(obj, key, {
4
+ value: value,
5
+ enumerable: true,
6
+ configurable: true,
7
+ writable: true
8
+ });
9
+ } else {
10
+ obj[key] = value;
11
+ }
12
+ return obj;
13
+ }
14
+ import { FormBuilderModule } from './form-builder.module';
15
+ import { FORM_BUILDER_MODULE_OPTIONS } from '../config/form-builder.constants';
16
+ import { FormController, FormResultController } from '../controllers';
17
+ function providerTokens(providers) {
18
+ return providers.map((p)=>typeof p === 'function' ? p : p.provide);
19
+ }
20
+ describe('FormBuilderModule', ()=>{
21
+ describe('forRoot', ()=>{
22
+ it('should default to a non-global module with all controllers registered', ()=>{
23
+ const dynamicModule = FormBuilderModule.forRoot({});
24
+ expect(dynamicModule.module).toBe(FormBuilderModule);
25
+ expect(dynamicModule.global).toBe(false);
26
+ expect(dynamicModule.controllers).toEqual([
27
+ FormController,
28
+ FormResultController
29
+ ]);
30
+ });
31
+ it('should respect global: true', ()=>{
32
+ expect(FormBuilderModule.forRoot({
33
+ global: true
34
+ }).global).toBe(true);
35
+ });
36
+ it('should register no controllers when includeController is false', ()=>{
37
+ expect(FormBuilderModule.forRoot({
38
+ includeController: false
39
+ }).controllers).toEqual([]);
40
+ });
41
+ it('should register a FORM_BUILDER_MODULE_OPTIONS provider carrying the given options', ()=>{
42
+ const options = {
43
+ bootstrapAppConfig: {
44
+ enableCompanyFeature: true
45
+ }
46
+ };
47
+ const dynamicModule = FormBuilderModule.forRoot(options);
48
+ const provider = dynamicModule.providers.find((p)=>p.provide === FORM_BUILDER_MODULE_OPTIONS);
49
+ expect(provider.useValue).toBe(options);
50
+ });
51
+ it('should always export the same fixed set of services', ()=>{
52
+ expect(FormBuilderModule.forRoot({}).exports).toHaveLength(4);
53
+ });
54
+ });
55
+ describe('forRootAsync', ()=>{
56
+ it('should forward external imports alongside CacheModule/UtilsModule', ()=>{
57
+ let FakeImportedModule = class FakeImportedModule {
58
+ };
59
+ const dynamicModule = FormBuilderModule.forRootAsync({
60
+ imports: [
61
+ FakeImportedModule
62
+ ]
63
+ });
64
+ expect(dynamicModule.imports).toEqual(expect.arrayContaining([
65
+ FakeImportedModule
66
+ ]));
67
+ });
68
+ it('useFactory: builds a FORM_BUILDER_MODULE_OPTIONS provider merging resolved config', async ()=>{
69
+ const useFactory = jest.fn().mockResolvedValue({
70
+ foo: 'bar'
71
+ });
72
+ const dynamicModule = FormBuilderModule.forRootAsync({
73
+ useFactory,
74
+ inject: [
75
+ 'SOME_TOKEN'
76
+ ]
77
+ });
78
+ const provider = dynamicModule.providers.find((p)=>p.provide === FORM_BUILDER_MODULE_OPTIONS);
79
+ expect(provider.inject).toEqual([
80
+ 'SOME_TOKEN'
81
+ ]);
82
+ const resolved = await provider.useFactory('injected');
83
+ expect(useFactory).toHaveBeenCalledWith('injected');
84
+ expect(resolved.config).toEqual({
85
+ foo: 'bar'
86
+ });
87
+ });
88
+ it('useClass: registers both the class provider and an options provider calling createFormBuilderOptions()', async ()=>{
89
+ let MyOptionsFactory = class MyOptionsFactory {
90
+ constructor(){
91
+ _define_property(this, "createFormBuilderOptions", jest.fn().mockResolvedValue({
92
+ foo: 'from-class'
93
+ }));
94
+ }
95
+ };
96
+ const dynamicModule = FormBuilderModule.forRootAsync({
97
+ useClass: MyOptionsFactory
98
+ });
99
+ const classProvider = dynamicModule.providers.find((p)=>p.provide === MyOptionsFactory);
100
+ expect(classProvider.useClass).toBe(MyOptionsFactory);
101
+ const optionsProvider = dynamicModule.providers.find((p)=>p.provide === FORM_BUILDER_MODULE_OPTIONS);
102
+ const instance = new MyOptionsFactory();
103
+ const resolved = await optionsProvider.useFactory(instance);
104
+ expect(instance.createFormBuilderOptions).toHaveBeenCalled();
105
+ expect(resolved.config).toEqual({
106
+ foo: 'from-class'
107
+ });
108
+ });
109
+ it('useExisting: registers a single options provider without a duplicate class registration', ()=>{
110
+ let MyOptionsFactory = class MyOptionsFactory {
111
+ constructor(){
112
+ _define_property(this, "createFormBuilderOptions", jest.fn());
113
+ }
114
+ };
115
+ const dynamicModule = FormBuilderModule.forRootAsync({
116
+ useExisting: MyOptionsFactory
117
+ });
118
+ expect(dynamicModule.providers.filter((p)=>p.provide === MyOptionsFactory)).toHaveLength(0);
119
+ const optionsProvider = dynamicModule.providers.find((p)=>p.provide === FORM_BUILDER_MODULE_OPTIONS);
120
+ expect(optionsProvider.inject).toEqual([
121
+ MyOptionsFactory
122
+ ]);
123
+ });
124
+ it('should register no FORM_BUILDER_MODULE_OPTIONS provider when neither useFactory, useClass, nor useExisting is given', ()=>{
125
+ const dynamicModule = FormBuilderModule.forRootAsync({});
126
+ expect(providerTokens(dynamicModule.providers)).not.toContain(FORM_BUILDER_MODULE_OPTIONS);
127
+ });
128
+ });
129
+ });
@@ -0,0 +1,117 @@
1
+ import { FormBuilderConfigService } from './form-builder-config.service';
2
+ function buildOptions(overrides = {}) {
3
+ return {
4
+ bootstrapAppConfig: undefined,
5
+ config: undefined,
6
+ ...overrides
7
+ };
8
+ }
9
+ describe('FormBuilderConfigService', ()=>{
10
+ describe('isCompanyFeatureEnabled', ()=>{
11
+ it('returns false when neither tenant nor bootstrap config enable the feature', ()=>{
12
+ const service = new FormBuilderConfigService(buildOptions());
13
+ expect(service.isCompanyFeatureEnabled()).toBe(false);
14
+ });
15
+ it('returns true when bootstrapAppConfig enables the feature', ()=>{
16
+ const service = new FormBuilderConfigService(buildOptions({
17
+ bootstrapAppConfig: {
18
+ databaseMode: 'single',
19
+ enableCompanyFeature: true
20
+ }
21
+ }));
22
+ expect(service.isCompanyFeatureEnabled()).toBe(true);
23
+ });
24
+ it('prefers the tenant override over bootstrapAppConfig when a tenant is passed', ()=>{
25
+ const service = new FormBuilderConfigService(buildOptions({
26
+ bootstrapAppConfig: {
27
+ databaseMode: 'multi-tenant',
28
+ enableCompanyFeature: false
29
+ }
30
+ }));
31
+ const result = service.isCompanyFeatureEnabled({
32
+ enableCompanyFeature: true
33
+ });
34
+ expect(result).toBe(true);
35
+ });
36
+ it('falls back to bootstrapAppConfig when tenant does not specify enableCompanyFeature', ()=>{
37
+ const service = new FormBuilderConfigService(buildOptions({
38
+ bootstrapAppConfig: {
39
+ databaseMode: 'multi-tenant',
40
+ enableCompanyFeature: true
41
+ }
42
+ }));
43
+ const result = service.isCompanyFeatureEnabled({
44
+ id: 'tenant-1'
45
+ });
46
+ expect(result).toBe(true);
47
+ });
48
+ });
49
+ describe('getDatabaseMode', ()=>{
50
+ it('returns the configured database mode', ()=>{
51
+ const service = new FormBuilderConfigService(buildOptions({
52
+ bootstrapAppConfig: {
53
+ databaseMode: 'multi-tenant'
54
+ }
55
+ }));
56
+ expect(service.getDatabaseMode()).toBe('multi-tenant');
57
+ });
58
+ it('defaults to "single" when bootstrapAppConfig is missing', ()=>{
59
+ const service = new FormBuilderConfigService(buildOptions({
60
+ bootstrapAppConfig: undefined
61
+ }));
62
+ expect(service.getDatabaseMode()).toBe('single');
63
+ });
64
+ it('defaults to "single" when databaseMode is not set on bootstrapAppConfig', ()=>{
65
+ const service = new FormBuilderConfigService(buildOptions({
66
+ bootstrapAppConfig: {}
67
+ }));
68
+ expect(service.getDatabaseMode()).toBe('single');
69
+ });
70
+ });
71
+ describe('isMultiTenant', ()=>{
72
+ it('returns true when database mode is multi-tenant', ()=>{
73
+ const service = new FormBuilderConfigService(buildOptions({
74
+ bootstrapAppConfig: {
75
+ databaseMode: 'multi-tenant'
76
+ }
77
+ }));
78
+ expect(service.isMultiTenant()).toBe(true);
79
+ });
80
+ it('returns false when database mode is single', ()=>{
81
+ const service = new FormBuilderConfigService(buildOptions({
82
+ bootstrapAppConfig: {
83
+ databaseMode: 'single'
84
+ }
85
+ }));
86
+ expect(service.isMultiTenant()).toBe(false);
87
+ });
88
+ });
89
+ describe('getOptions', ()=>{
90
+ it('returns the exact options object it was constructed with', ()=>{
91
+ const options = buildOptions({
92
+ config: {
93
+ tenants: []
94
+ }
95
+ });
96
+ const service = new FormBuilderConfigService(options);
97
+ expect(service.getOptions()).toBe(options);
98
+ });
99
+ });
100
+ describe('getConfig', ()=>{
101
+ it('returns the config sub-object', ()=>{
102
+ const config = {
103
+ tenants: []
104
+ };
105
+ const service = new FormBuilderConfigService(buildOptions({
106
+ config
107
+ }));
108
+ expect(service.getConfig()).toBe(config);
109
+ });
110
+ it('returns undefined when config was never provided', ()=>{
111
+ const service = new FormBuilderConfigService(buildOptions({
112
+ config: undefined
113
+ }));
114
+ expect(service.getConfig()).toBeUndefined();
115
+ });
116
+ });
117
+ });
@@ -0,0 +1,143 @@
1
+ import { FormBuilderDataSourceProvider } from './form-builder-datasource.provider';
2
+ import { Form, FormResult, FormWithCompany } from '../entities';
3
+ function buildConfigService(overrides = {}) {
4
+ const options = {
5
+ bootstrapAppConfig: {
6
+ databaseMode: 'single'
7
+ },
8
+ config: {
9
+ defaultDatabaseConfig: {
10
+ type: 'mysql',
11
+ host: 'localhost',
12
+ port: 3306,
13
+ username: 'root',
14
+ password: 'password',
15
+ database: 'flusys_test'
16
+ },
17
+ tenantDefaultDatabaseConfig: undefined,
18
+ tenants: []
19
+ }
20
+ };
21
+ return {
22
+ getOptions: jest.fn().mockReturnValue(options),
23
+ isCompanyFeatureEnabled: jest.fn().mockReturnValue(overrides.isCompanyFeatureEnabled ?? false)
24
+ };
25
+ }
26
+ describe('FormBuilderDataSourceProvider', ()=>{
27
+ afterEach(()=>{
28
+ // Reset the isolated static caches this provider owns so tests don't leak state between runs.
29
+ FormBuilderDataSourceProvider.tenantConnections = new Map();
30
+ FormBuilderDataSourceProvider.singleDataSource = null;
31
+ FormBuilderDataSourceProvider.tenantsRegistry = new Map();
32
+ FormBuilderDataSourceProvider.initialized = false;
33
+ FormBuilderDataSourceProvider.connectionLocks = new Map();
34
+ FormBuilderDataSourceProvider.singleConnectionLock = null;
35
+ });
36
+ it('constructs and forwards bootstrap/database config to the base class', ()=>{
37
+ const configService = buildConfigService();
38
+ const provider = new FormBuilderDataSourceProvider(configService, undefined);
39
+ expect(configService.getOptions).toHaveBeenCalled();
40
+ expect(provider.getDatabaseMode()).toBe('single');
41
+ expect(provider.isMultiTenant()).toBe(false);
42
+ });
43
+ it('reflects multi-tenant mode from bootstrapAppConfig', ()=>{
44
+ const configService = buildConfigService();
45
+ configService.getOptions.mockReturnValue({
46
+ bootstrapAppConfig: {
47
+ databaseMode: 'multi-tenant'
48
+ },
49
+ config: {}
50
+ });
51
+ const provider = new FormBuilderDataSourceProvider(configService, undefined);
52
+ expect(provider.isMultiTenant()).toBe(true);
53
+ });
54
+ it('owns isolated static caches, separate from the base MultiTenantDataSourceService', ()=>{
55
+ // Each DataSource Provider must override BOTH static cache properties and the
56
+ // methods that read them — otherwise modules would share connections (see FLUSYS_NEST/CLAUDE.md).
57
+ expect(FormBuilderDataSourceProvider.tenantConnections).not.toBe(Object.getPrototypeOf(FormBuilderDataSourceProvider).tenantConnections);
58
+ });
59
+ describe('getFormBuilderEntities', ()=>{
60
+ it('returns core entities (no company feature) using the config service default', async ()=>{
61
+ const configService = buildConfigService({
62
+ isCompanyFeatureEnabled: false
63
+ });
64
+ const provider = new FormBuilderDataSourceProvider(configService, undefined);
65
+ const entities = await provider.getFormBuilderEntities();
66
+ expect(entities).toEqual([
67
+ Form,
68
+ FormResult
69
+ ]);
70
+ });
71
+ it('returns company entities using the config service default', async ()=>{
72
+ const configService = buildConfigService({
73
+ isCompanyFeatureEnabled: true
74
+ });
75
+ const provider = new FormBuilderDataSourceProvider(configService, undefined);
76
+ const entities = await provider.getFormBuilderEntities();
77
+ expect(entities).toEqual([
78
+ FormWithCompany,
79
+ FormResult
80
+ ]);
81
+ });
82
+ it('allows an explicit enableCompanyFeature override, bypassing config service', async ()=>{
83
+ const configService = buildConfigService({
84
+ isCompanyFeatureEnabled: false
85
+ });
86
+ const provider = new FormBuilderDataSourceProvider(configService, undefined);
87
+ const entities = await provider.getFormBuilderEntities(true);
88
+ expect(entities).toEqual([
89
+ FormWithCompany,
90
+ FormResult
91
+ ]);
92
+ expect(configService.isCompanyFeatureEnabled).not.toHaveBeenCalled();
93
+ });
94
+ });
95
+ describe('createDataSourceFromConfig', ()=>{
96
+ it('builds a DataSource using core entities when company feature disabled', async ()=>{
97
+ const configService = buildConfigService({
98
+ isCompanyFeatureEnabled: false
99
+ });
100
+ const provider = new FormBuilderDataSourceProvider(configService, undefined);
101
+ const fakeDataSource = {
102
+ isInitialized: true
103
+ };
104
+ const superSpy = jest.spyOn(Object.getPrototypeOf(Object.getPrototypeOf(provider)), 'createDataSourceFromConfig').mockResolvedValue(fakeDataSource);
105
+ const config = {
106
+ type: 'mysql',
107
+ host: 'localhost',
108
+ port: 3306,
109
+ username: 'root',
110
+ password: 'password',
111
+ database: 'flusys_test'
112
+ };
113
+ const result = await provider.createDataSourceFromConfig(config);
114
+ expect(superSpy).toHaveBeenCalledWith(config, [
115
+ Form,
116
+ FormResult
117
+ ]);
118
+ expect(result).toBe(fakeDataSource);
119
+ superSpy.mockRestore();
120
+ });
121
+ it('builds a DataSource using company entities when company feature enabled', async ()=>{
122
+ const configService = buildConfigService({
123
+ isCompanyFeatureEnabled: true
124
+ });
125
+ const provider = new FormBuilderDataSourceProvider(configService, undefined);
126
+ const fakeDataSource = {
127
+ isInitialized: true
128
+ };
129
+ const superSpy = jest.spyOn(Object.getPrototypeOf(Object.getPrototypeOf(provider)), 'createDataSourceFromConfig').mockResolvedValue(fakeDataSource);
130
+ const config = {
131
+ type: 'mysql',
132
+ database: 'flusys_test'
133
+ };
134
+ const result = await provider.createDataSourceFromConfig(config);
135
+ expect(superSpy).toHaveBeenCalledWith(config, [
136
+ FormWithCompany,
137
+ FormResult
138
+ ]);
139
+ expect(result).toBe(fakeDataSource);
140
+ superSpy.mockRestore();
141
+ });
142
+ });
143
+ });