Skip to content

Commit 8192225

Browse files
marianfoonlunets
andauthored
feat: add tenant-specific data loading functionality (#960)
* feat: add tenant-specific data loading functionality * Create popular-elephants-admire.md * fix: linting --------- Co-authored-by: Nicolas Lunet <[email protected]>
1 parent b9a6045 commit 8192225

File tree

7 files changed

+212
-31
lines changed

7 files changed

+212
-31
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@sap-ux/fe-mockserver-core": patch
3+
---
4+
5+
feat: add tenant-specific data loading functionality

docs/DefiningMockdata.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
## Influencing entity set behavior
55

66
By default the mockserver will load JSON files for your entity set and return them as is. If you use context based isolation the mockserver will also try to load a specific file for the tenant you are working on.
7+
8+
**Tenant-specific files**: You can provide only tenant-specific files (e.g., `EntityName-100.json`, `EntityName-200.json`) without requiring a base file (`EntityName.json`). The mockserver will load the appropriate tenant file based on the context. If no tenant-specific file exists for the requested tenant, an empty dataset will be returned.
9+
710
However if you want to influence the behavior of the mockserver you can do so by defining your own mockdata file
811

912
The mockserver allows you to define your mock data as javascript file and function that allow you to influence the behavior of the standard function to match your needs.

packages/fe-mockserver-core/src/data/entitySets/entitySet.ts

Lines changed: 78 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,63 @@ function prepareLiteral(literal: string, propertyType: string) {
134134
return literal;
135135
}
136136
}
137+
138+
/**
139+
* Creates a tenant data loader function that handles loading and processing tenant-specific JSON files.
140+
*
141+
* @param mockDataRootFolder - The root folder for mock data files
142+
* @param entity - The entity name to load data for
143+
* @param isDraft - Whether the entity is a draft entity
144+
* @param dataAccess - The data access interface for file operations
145+
* @param fallbackData - The fallback data to return if no tenant file exists
146+
* @param generateMockData - Whether to enable mock data generation when no data exists
147+
* @returns A function that loads tenant-specific data based on context ID
148+
*/
149+
function createTenantDataLoader(
150+
mockDataRootFolder: string,
151+
entity: string,
152+
isDraft: boolean,
153+
dataAccess: DataAccessInterface,
154+
fallbackData: any[],
155+
generateMockData?: boolean
156+
) {
157+
return function (contextId: string) {
158+
if (dataAccess.fileLoader.syncSupported()) {
159+
const fileNameSuffix = contextId === 'tenant-default' ? '' : `-${contextId.replace('tenant-', '')}`;
160+
const targetFile = join(mockDataRootFolder, entity + fileNameSuffix) + '.json';
161+
if (dataAccess.fileLoader.existsSync(targetFile)) {
162+
const jsonFileContent = dataAccess.fileLoader.loadFileSync(targetFile);
163+
let tenantJsonData: any[];
164+
if (jsonFileContent.length === 0) {
165+
tenantJsonData = [];
166+
} else {
167+
tenantJsonData = JSON.parse(jsonFileContent);
168+
169+
tenantJsonData.forEach((jsonLine) => {
170+
if (isDraft) {
171+
const IsActiveEntityValue = jsonLine.IsActiveEntity;
172+
if (IsActiveEntityValue === undefined) {
173+
jsonLine.IsActiveEntity = true;
174+
jsonLine.HasActiveEntity = true;
175+
jsonLine.HasDraftEntity = false;
176+
}
177+
}
178+
delete jsonLine['@odata.etag'];
179+
});
180+
}
181+
return tenantJsonData;
182+
}
183+
}
184+
// No tenant file found, return fallback data
185+
const result = fallbackData.concat();
186+
// If fallback data is empty and generateMockData is true, preserve the flag
187+
if (result.length === 0 && generateMockData) {
188+
(result as any).__generateMockData = generateMockData;
189+
}
190+
return result;
191+
};
192+
}
193+
137194
/**
138195
*
139196
*/
@@ -195,42 +252,32 @@ export class MockDataEntitySet implements EntitySetInterface {
195252
outData = {}; // set as an object in all case
196253
}
197254
isInitial = false;
198-
(outData as any).getInitialDataSet = function (contextId: string) {
199-
if (dataAccess.fileLoader.syncSupported()) {
200-
const fileNameSuffix =
201-
contextId === 'tenant-default' ? '' : `-${contextId.replace('tenant-', '')}`;
202-
const targetFile = join(mockDataRootFolder, entity + fileNameSuffix) + '.json';
203-
if (dataAccess.fileLoader.existsSync(targetFile)) {
204-
const jsonFileContent = dataAccess.fileLoader.loadFileSync(targetFile);
205-
let tenantJsonData: any[];
206-
if (fileContent.length === 0) {
207-
tenantJsonData = [];
208-
} else {
209-
tenantJsonData = JSON.parse(jsonFileContent);
210-
211-
tenantJsonData.forEach((jsonLine) => {
212-
if (isDraft) {
213-
const IsActiveEntityValue = jsonLine.IsActiveEntity;
214-
if (IsActiveEntityValue === undefined) {
215-
jsonLine.IsActiveEntity = true;
216-
jsonLine.HasActiveEntity = true;
217-
jsonLine.HasDraftEntity = false;
218-
}
219-
}
220-
delete jsonLine['@odata.etag'];
221-
});
222-
}
223-
return tenantJsonData;
224-
}
225-
}
226-
227-
return outJsonData.concat();
228-
};
255+
(outData as any).getInitialDataSet = createTenantDataLoader(
256+
mockDataRootFolder,
257+
entity,
258+
isDraft,
259+
dataAccess,
260+
outJsonData,
261+
generateMockData
262+
);
229263
// }
230264
} catch (e) {
231265
dataAccess.log.info(e as string);
232266
}
233267
}
268+
// Create getInitialDataSet for tenant files if not already created
269+
if (isInitial && !(outData as any).getInitialDataSet) {
270+
outData = {};
271+
isInitial = false;
272+
(outData as any).getInitialDataSet = createTenantDataLoader(
273+
mockDataRootFolder,
274+
entity,
275+
isDraft,
276+
dataAccess,
277+
[], // No base data fallback
278+
generateMockData
279+
);
280+
}
234281
if (isInitial) {
235282
dataAccess.log.info(`No JS or Json file found for ${entity} at ${jsonFilePath}`);
236283
outData = [];
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[
2+
{
3+
"ID": 1,
4+
"name": "Tenant 100 Entity 1",
5+
"description": "This is entity 1 for tenant 100"
6+
},
7+
{
8+
"ID": 2,
9+
"name": "Tenant 100 Entity 2",
10+
"description": "This is entity 2 for tenant 100"
11+
}
12+
]
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[
2+
{
3+
"ID": 3,
4+
"name": "Tenant 200 Entity 3",
5+
"description": "This is entity 3 for tenant 200"
6+
}
7+
]

packages/fe-mockserver-core/test/unit/data/mockdata/service.cds

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,10 @@ service MultiLevelExpand {
2828
MyValue: String(17);
2929
MyValueNotNull: String(17) not null;
3030
}
31+
32+
entity TenantOnlyEntity {
33+
key ID : Integer;
34+
name : String(100);
35+
description : String(500);
36+
}
3137
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import CDSMetadataProvider from '@sap-ux/fe-mockserver-plugin-cds';
2+
import { join } from 'path';
3+
import Router from 'router';
4+
import type { ServiceConfig } from '../../../src';
5+
import type { EntitySetInterface } from '../../../src/data/common';
6+
import { DataAccess } from '../../../src/data/dataAccess';
7+
import { ODataMetadata } from '../../../src/data/metadata';
8+
import { ServiceRegistry } from '../../../src/data/serviceRegistry';
9+
import FileSystemLoader from '../../../src/plugins/fileSystemLoader';
10+
import ODataRequest from '../../../src/request/odataRequest';
11+
12+
let metadata!: ODataMetadata;
13+
const baseUrl = '/sap/fe/mock';
14+
15+
describe('Tenant-Only Mock Data', () => {
16+
const fileLoader = new FileSystemLoader();
17+
const baseDir = join(__dirname, 'mockdata');
18+
const metadataProvider = new CDSMetadataProvider(fileLoader);
19+
let dataAccess: DataAccess;
20+
let tenantOnlyEntitySet: EntitySetInterface;
21+
22+
beforeAll(async () => {
23+
const edmx = await metadataProvider.loadMetadata(join(baseDir, 'service.cds'));
24+
metadata = await ODataMetadata.parse(edmx, baseUrl + '/$metadata');
25+
const app = new Router();
26+
const serviceRegistry = new ServiceRegistry(fileLoader, metadataProvider, app);
27+
dataAccess = new DataAccess(
28+
{ mockdataPath: baseDir } as ServiceConfig,
29+
metadata,
30+
fileLoader,
31+
undefined,
32+
serviceRegistry
33+
);
34+
tenantOnlyEntitySet = await dataAccess.getMockEntitySet('TenantOnlyEntity');
35+
});
36+
37+
it('can load tenant-specific data when no base file exists', async () => {
38+
// Test tenant-100 (has TenantOnlyEntity-100.json)
39+
const request100 = new ODataRequest(
40+
{
41+
method: 'GET',
42+
url: '/TenantOnlyEntity',
43+
tenantId: 'tenant-100'
44+
},
45+
dataAccess
46+
);
47+
48+
const mockData100 = (await tenantOnlyEntitySet.getMockData('tenant-100').getAllEntries(request100)) as any[];
49+
expect(mockData100).toHaveLength(2);
50+
expect(mockData100[0].name).toBe('Tenant 100 Entity 1');
51+
expect(mockData100[1].name).toBe('Tenant 100 Entity 2');
52+
});
53+
54+
it('can load different tenant-specific data', async () => {
55+
// Test tenant-200 (has TenantOnlyEntity-200.json)
56+
const request200 = new ODataRequest(
57+
{
58+
method: 'GET',
59+
url: '/TenantOnlyEntity',
60+
tenantId: 'tenant-200'
61+
},
62+
dataAccess
63+
);
64+
65+
const mockData200 = (await tenantOnlyEntitySet.getMockData('tenant-200').getAllEntries(request200)) as any[];
66+
expect(mockData200).toHaveLength(1);
67+
expect(mockData200[0].name).toBe('Tenant 200 Entity 3');
68+
});
69+
70+
it('returns empty array for tenant without specific file', async () => {
71+
// Test tenant-999 (no TenantOnlyEntity-999.json file exists)
72+
const request999 = new ODataRequest(
73+
{
74+
method: 'GET',
75+
url: '/TenantOnlyEntity',
76+
tenantId: 'tenant-999'
77+
},
78+
dataAccess
79+
);
80+
81+
const mockData999 = (await tenantOnlyEntitySet.getMockData('tenant-999').getAllEntries(request999)) as any[];
82+
expect(mockData999).toHaveLength(0);
83+
});
84+
85+
it('returns empty array for default tenant when no base file exists', async () => {
86+
// Test default tenant (no TenantOnlyEntity.json base file exists)
87+
const requestDefault = new ODataRequest(
88+
{
89+
method: 'GET',
90+
url: '/TenantOnlyEntity',
91+
tenantId: 'tenant-default'
92+
},
93+
dataAccess
94+
);
95+
96+
const mockDataDefault = (await tenantOnlyEntitySet
97+
.getMockData('tenant-default')
98+
.getAllEntries(requestDefault)) as any[];
99+
expect(mockDataDefault).toHaveLength(0);
100+
});
101+
});

0 commit comments

Comments
 (0)