@izara_project/izara-core-generate-service-code 1.0.57 → 1.0.59
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/package.json +1 -1
- package/src/__tests__/getIntTestConfig.test.js +51 -0
- package/src/__tests__/intTestGeneratorShared.test.cjs +169 -0
- package/src/__tests__/pathIntTest.test.js +103 -0
- package/src/codeGenerators/app/src/generatedCode/Flow/FlowSchemas/WebSocketGenerator.js +4 -0
- package/src/codeGenerators/app/src/generatedCode/SystemFlowSchemas/RegisterGenerator.js +6 -1
- package/src/codeGenerators/resource/sls_yaml/FlowResourceYamlGenerator.js +1 -1
- package/src/generate.js +40 -40
- package/src/generateCode.js +77 -68
- package/src/generateIntTest.js +35 -3
- package/src/generateResourceIntTest.js +38 -6
- package/src/generateResources/IntTest/generateTests/generateTemplate.js +18 -4
- package/src/generateResources/IntTest/generateTests/pathIntTest/pathIntTest.js +20 -32
- package/src/generateResources/IntTest/libs/libs.js +10 -2
- package/src/generateResources/IntTest/upload/uploadIntTest.js +82 -0
- package/src/generateSchema.js +78 -18
- package/src/intTestGeneratorShared.js +39 -9
- package/src/parsers/flowSchemaParser.js +6 -0
- package/src/schemaGenerators/app/src/schemas/ObjectSchemas/templates/DynamicAttributeTreeStandardSchemaTemplate.ejs +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@izara_project/izara-core-generate-service-code",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.59",
|
|
4
4
|
"description": "Code for locally generating per service files",
|
|
5
5
|
"homepage": "https://bitbucket.org/izara-core-support-services/izara-core-support-services-generate-service-code#readme",
|
|
6
6
|
"repository": {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import libs from '../generateResources/IntTest/libs/libs.js';
|
|
6
|
+
import { getObjectSchema } from '@izara_project/izara-core-library-service-schemas';
|
|
7
|
+
|
|
8
|
+
const { getIntTestConfig } = libs;
|
|
9
|
+
|
|
10
|
+
describe('getIntTestConfig path resolution', () => {
|
|
11
|
+
let originalGetDataFromPath;
|
|
12
|
+
let passedArgs;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
originalGetDataFromPath = getObjectSchema.getDataFromPath;
|
|
16
|
+
passedArgs = null;
|
|
17
|
+
getObjectSchema.getDataFromPath = async (_izContext, folderName, rootFolderPath, options) => {
|
|
18
|
+
passedArgs = { folderName, rootFolderPath, options };
|
|
19
|
+
return [];
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
getObjectSchema.getDataFromPath = originalGetDataFromPath;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('Case 1: should resolve correctly when rootPath includes generateIntegrationTest', async () => {
|
|
28
|
+
const rootPath = '/home/ironfist/Pong/Izara Project/Izara-Market-Product/izara-market-products-sell-offer-price-pong/app/src/generateIntegrationTest/';
|
|
29
|
+
await getIntTestConfig({ logger: console }, 'generateIntegrationTest/libs/', rootPath);
|
|
30
|
+
|
|
31
|
+
expect(passedArgs).not.toBeNull();
|
|
32
|
+
expect(passedArgs.folderName).toBe('libs');
|
|
33
|
+
expect(passedArgs.rootFolderPath).toBe('/home/ironfist/Pong/Izara Project/Izara-Market-Product/izara-market-products-sell-offer-price-pong/app/src/generateIntegrationTest');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('Case 2: should resolve correctly when rootPath is repo root with app/src/... structure', async () => {
|
|
37
|
+
const tmpDir = path.join(os.tmpdir(), 'get-int-test-config-scratch');
|
|
38
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
39
|
+
fs.mkdirSync(path.join(tmpDir, 'app', 'src', 'generateIntegrationTest', 'libs'), { recursive: true });
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
await getIntTestConfig({ logger: console }, 'generateIntegrationTest/libs/', tmpDir);
|
|
43
|
+
|
|
44
|
+
expect(passedArgs).not.toBeNull();
|
|
45
|
+
expect(passedArgs.folderName).toBe('libs');
|
|
46
|
+
expect(passedArgs.rootFolderPath).toBe(path.join(tmpDir, 'app', 'src', 'generateIntegrationTest'));
|
|
47
|
+
} finally {
|
|
48
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
let getIntTestOutputPath;
|
|
6
|
+
let runTemplateItems;
|
|
7
|
+
|
|
8
|
+
const tempDirs = [];
|
|
9
|
+
|
|
10
|
+
beforeAll(async () => {
|
|
11
|
+
({ getIntTestOutputPath, runTemplateItems } =
|
|
12
|
+
await import('../intTestGeneratorShared.js'));
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
for (const dir of tempDirs.splice(0)) {
|
|
17
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('getIntTestOutputPath', () => {
|
|
22
|
+
it('uses app/src when app directory exists', () => {
|
|
23
|
+
const rootPath = fs.mkdtempSync(
|
|
24
|
+
path.join(os.tmpdir(), 'int-test-output-app-')
|
|
25
|
+
);
|
|
26
|
+
tempDirs.push(rootPath);
|
|
27
|
+
fs.mkdirSync(path.join(rootPath, 'app'));
|
|
28
|
+
|
|
29
|
+
expect(getIntTestOutputPath(rootPath)).toBe(
|
|
30
|
+
path.join(rootPath, 'app', 'src', 'generateIntegrationTest')
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('falls back to src when app directory is missing', () => {
|
|
35
|
+
const rootPath = fs.mkdtempSync(
|
|
36
|
+
path.join(os.tmpdir(), 'int-test-output-src-')
|
|
37
|
+
);
|
|
38
|
+
tempDirs.push(rootPath);
|
|
39
|
+
|
|
40
|
+
expect(getIntTestOutputPath(rootPath)).toBe(
|
|
41
|
+
path.join(rootPath, 'src', 'generateIntegrationTest')
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('detects app from project root even when output path is different', () => {
|
|
46
|
+
const projectRootPath = fs.mkdtempSync(
|
|
47
|
+
path.join(os.tmpdir(), 'int-test-output-project-root-')
|
|
48
|
+
);
|
|
49
|
+
const outputPath = fs.mkdtempSync(
|
|
50
|
+
path.join(os.tmpdir(), 'int-test-output-custom-')
|
|
51
|
+
);
|
|
52
|
+
tempDirs.push(projectRootPath, outputPath);
|
|
53
|
+
fs.mkdirSync(path.join(projectRootPath, 'app'));
|
|
54
|
+
|
|
55
|
+
expect(getIntTestOutputPath(outputPath, projectRootPath)).toBe(
|
|
56
|
+
path.join(outputPath, 'app', 'src', 'generateIntegrationTest')
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('runTemplateItems', () => {
|
|
62
|
+
it('skips empty array items', async () => {
|
|
63
|
+
const writeSource = jest.fn();
|
|
64
|
+
|
|
65
|
+
await runTemplateItems([[]], {
|
|
66
|
+
writeSource,
|
|
67
|
+
errorPrefix: 'IntTest resource'
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
expect(writeSource).not.toHaveBeenCalled();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('validates items before writing', async () => {
|
|
74
|
+
const writeSource = jest.fn()
|
|
75
|
+
.mockResolvedValueOnce([true, {}])
|
|
76
|
+
.mockResolvedValueOnce([true, {}]);
|
|
77
|
+
|
|
78
|
+
await runTemplateItems([
|
|
79
|
+
{
|
|
80
|
+
templatePath: '/tmp/template.ejs',
|
|
81
|
+
templateData: { ok: true },
|
|
82
|
+
setting: { savePath: '/tmp/out' }
|
|
83
|
+
}
|
|
84
|
+
], {
|
|
85
|
+
writeSource,
|
|
86
|
+
errorPrefix: 'IntTest resource'
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(writeSource).toHaveBeenNthCalledWith(1, expect.any(Object), {
|
|
90
|
+
checkCreateSourcePass: true
|
|
91
|
+
});
|
|
92
|
+
expect(writeSource).toHaveBeenNthCalledWith(2, expect.any(Object), {
|
|
93
|
+
checkCreateSourcePass: false
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('preserves configured files when clearing a save path', async () => {
|
|
98
|
+
const savePath = fs.mkdtempSync(
|
|
99
|
+
path.join(os.tmpdir(), 'int-test-preserve-save-path-')
|
|
100
|
+
);
|
|
101
|
+
tempDirs.push(savePath);
|
|
102
|
+
fs.writeFileSync(path.join(savePath, 'saveConfig.js'), 'keep');
|
|
103
|
+
fs.writeFileSync(path.join(savePath, 'old.js'), 'remove');
|
|
104
|
+
fs.mkdirSync(path.join(savePath, 'nested'));
|
|
105
|
+
fs.writeFileSync(path.join(savePath, 'nested', 'stale.json'), '{}');
|
|
106
|
+
|
|
107
|
+
const writeSource = jest.fn()
|
|
108
|
+
.mockResolvedValueOnce([true, {}])
|
|
109
|
+
.mockImplementationOnce(async ({ setting }) => {
|
|
110
|
+
fs.writeFileSync(path.join(setting.savePath, 'IntTestPath.js'), 'new');
|
|
111
|
+
return [true, {}];
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
await runTemplateItems([
|
|
115
|
+
{
|
|
116
|
+
templatePath: '/tmp/template.ejs',
|
|
117
|
+
templateData: { ok: true },
|
|
118
|
+
setting: {
|
|
119
|
+
savePath,
|
|
120
|
+
preserveFileNames: ['saveConfig.js']
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
], {
|
|
124
|
+
writeSource,
|
|
125
|
+
errorPrefix: 'IntTest resource'
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
expect(fs.existsSync(path.join(savePath, 'saveConfig.js'))).toBe(true);
|
|
129
|
+
expect(fs.readFileSync(path.join(savePath, 'saveConfig.js'), 'utf8')).toBe('keep');
|
|
130
|
+
expect(fs.existsSync(path.join(savePath, 'old.js'))).toBe(false);
|
|
131
|
+
expect(fs.existsSync(path.join(savePath, 'nested'))).toBe(false);
|
|
132
|
+
expect(fs.existsSync(path.join(savePath, 'IntTestPath.js'))).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('does not clear save path when cleanSavePath is false', async () => {
|
|
136
|
+
const savePath = fs.mkdtempSync(
|
|
137
|
+
path.join(os.tmpdir(), 'int-test-no-clean-save-path-')
|
|
138
|
+
);
|
|
139
|
+
tempDirs.push(savePath);
|
|
140
|
+
fs.writeFileSync(path.join(savePath, 'config.json'), 'keep');
|
|
141
|
+
fs.mkdirSync(path.join(savePath, 'nested-dir'));
|
|
142
|
+
fs.writeFileSync(path.join(savePath, 'nested-dir', 'file.json'), '{}');
|
|
143
|
+
|
|
144
|
+
const writeSource = jest.fn()
|
|
145
|
+
.mockResolvedValueOnce([true, {}])
|
|
146
|
+
.mockImplementationOnce(async ({ setting }) => {
|
|
147
|
+
fs.writeFileSync(path.join(setting.savePath, 'IntTestPath.js'), 'new');
|
|
148
|
+
return [true, {}];
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
await runTemplateItems([
|
|
152
|
+
{
|
|
153
|
+
templatePath: '/tmp/template.ejs',
|
|
154
|
+
templateData: { ok: true },
|
|
155
|
+
setting: {
|
|
156
|
+
savePath,
|
|
157
|
+
cleanSavePath: false
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
], {
|
|
161
|
+
writeSource,
|
|
162
|
+
errorPrefix: 'IntTest resource'
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
expect(fs.existsSync(path.join(savePath, 'config.json'))).toBe(true);
|
|
166
|
+
expect(fs.existsSync(path.join(savePath, 'nested-dir'))).toBe(true);
|
|
167
|
+
expect(fs.existsSync(path.join(savePath, 'IntTestPath.js'))).toBe(true);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { describe, it, expect } from '@jest/globals';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
import generatePathIntTest from '../generateResources/IntTest/generateTests/pathIntTest/pathIntTest.js';
|
|
7
|
+
|
|
8
|
+
describe('generatePathIntTest', () => {
|
|
9
|
+
it('creates IntTestPath data from generated files in the new folder structure', async () => {
|
|
10
|
+
const rootPath = fs.mkdtempSync(path.join(os.tmpdir(), 'int-test-path-'));
|
|
11
|
+
const srcPath = path.join(rootPath, 'app', 'src', 'generateIntegrationTest');
|
|
12
|
+
const serviceTag = 'svc-demo';
|
|
13
|
+
|
|
14
|
+
fs.mkdirSync(path.join(srcPath, serviceTag, 'orderCreate', 'caseA'), { recursive: true });
|
|
15
|
+
fs.mkdirSync(path.join(srcPath, serviceTag, 'orderCreate', 'orderCreate'), { recursive: true });
|
|
16
|
+
fs.mkdirSync(path.join(srcPath, serviceTag, 'shared'), { recursive: true });
|
|
17
|
+
|
|
18
|
+
fs.writeFileSync(path.join(srcPath, serviceTag, 'orderCreate', 'caseA', 'events.json'), '{}');
|
|
19
|
+
fs.writeFileSync(path.join(srcPath, serviceTag, 'orderCreate', 'orderCreate', 'tests.json'), '{}');
|
|
20
|
+
fs.writeFileSync(path.join(srcPath, serviceTag, 'shared', 'resources.json'), '{}');
|
|
21
|
+
fs.writeFileSync(path.join(srcPath, serviceTag, 'ignore.txt'), 'ignore');
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const result = await generatePathIntTest({}, srcPath, {
|
|
25
|
+
serviceTag,
|
|
26
|
+
s3Prefix: 'demo-prefix'
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
expect(result).toEqual({
|
|
30
|
+
templateData: {
|
|
31
|
+
intTestPaths: [
|
|
32
|
+
'test_config/demo-prefix/svc-demo/orderCreate/caseA/',
|
|
33
|
+
'test_config/demo-prefix/svc-demo/orderCreate/orderCreate/',
|
|
34
|
+
'test_config/demo-prefix/svc-demo/shared/'
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
templatePath: expect.stringContaining('template.ejs'),
|
|
38
|
+
setting: {
|
|
39
|
+
generateHookFile: false,
|
|
40
|
+
savePath: path.join(srcPath, './libs/'),
|
|
41
|
+
saveFileName: 'IntTestPath',
|
|
42
|
+
fileExtension: '.js',
|
|
43
|
+
isAppend: false,
|
|
44
|
+
cleanSavePath: false
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
} finally {
|
|
48
|
+
fs.rmSync(rootPath, { recursive: true, force: true });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('preserves test case folder name prefix even if it contains the serviceTag', async () => {
|
|
53
|
+
const rootPath = fs.mkdtempSync(path.join(os.tmpdir(), 'int-test-path-nested-'));
|
|
54
|
+
const srcPath = path.join(rootPath, 'app', 'src', 'generateIntegrationTest');
|
|
55
|
+
const serviceTag = 'SellOfferManager';
|
|
56
|
+
|
|
57
|
+
// Mock folder structure similar to the issue reported
|
|
58
|
+
const nestedTestFolder = path.join(srcPath, serviceTag, 'IntegrationTest_SellOfferManager_ProcessCalCombinedPricingHdrSqs__case_BasicCase');
|
|
59
|
+
fs.mkdirSync(nestedTestFolder, { recursive: true });
|
|
60
|
+
fs.writeFileSync(path.join(nestedTestFolder, 'events.json'), '{}');
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const result = await generatePathIntTest({}, srcPath, {
|
|
64
|
+
serviceTag,
|
|
65
|
+
s3Prefix: 'PongDev'
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(result).toEqual({
|
|
69
|
+
templateData: {
|
|
70
|
+
intTestPaths: [
|
|
71
|
+
'test_config/PongDev/SellOfferManager/IntegrationTest_SellOfferManager_ProcessCalCombinedPricingHdrSqs__case_BasicCase/'
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
templatePath: expect.stringContaining('template.ejs'),
|
|
75
|
+
setting: {
|
|
76
|
+
generateHookFile: false,
|
|
77
|
+
savePath: path.join(srcPath, './libs/'),
|
|
78
|
+
saveFileName: 'IntTestPath',
|
|
79
|
+
fileExtension: '.js',
|
|
80
|
+
isAppend: false,
|
|
81
|
+
cleanSavePath: false
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
} finally {
|
|
85
|
+
fs.rmSync(rootPath, { recursive: true, force: true });
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('returns empty when no generated files exist for the service tag', async () => {
|
|
90
|
+
const rootPath = fs.mkdtempSync(path.join(os.tmpdir(), 'int-test-path-empty-'));
|
|
91
|
+
const srcPath = path.join(rootPath, 'src', 'generateIntegrationTest');
|
|
92
|
+
fs.mkdirSync(srcPath, { recursive: true });
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
await expect(generatePathIntTest({}, srcPath, {
|
|
96
|
+
serviceTag: 'missing-service',
|
|
97
|
+
s3Prefix: 'demo-prefix'
|
|
98
|
+
})).resolves.toEqual([]);
|
|
99
|
+
} finally {
|
|
100
|
+
fs.rmSync(rootPath, { recursive: true, force: true });
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -103,9 +103,13 @@ export async function generateWebSocket(allLocalFlowSchemas, appPath, outputBase
|
|
|
103
103
|
// 4. SQS and SNS Subscriptions
|
|
104
104
|
await renderAndAppend('ProcessWebSocket_Yaml.ejs', path.join(resourceYamlDir, 'generated-sns-in-sqs.yml'), processData);
|
|
105
105
|
|
|
106
|
+
const processedTopics = new Set();
|
|
106
107
|
for (const flowSchema of allLocalFlowSchemas) {
|
|
107
108
|
if (flowSchema.outputTopic) {
|
|
108
109
|
const topicOut = upperCase(flowSchema.flowTag);
|
|
110
|
+
if (processedTopics.has(topicOut)) continue;
|
|
111
|
+
processedTopics.add(topicOut);
|
|
112
|
+
|
|
109
113
|
await renderAndAppend('ProcessWebSocketSub_Yaml.ejs', path.join(resourceYamlDir, 'generated-sns-in-sqs.yml'), {
|
|
110
114
|
subscribeTo: topicOut,
|
|
111
115
|
queueName: queueName,
|
|
@@ -126,14 +126,19 @@ export async function generateRegisterFlow(allLocalFlowSchemas, _appPath, output
|
|
|
126
126
|
completeData
|
|
127
127
|
);
|
|
128
128
|
|
|
129
|
+
const processedTopics = new Set();
|
|
129
130
|
for (const flowSchema of allLocalFlowSchemas) {
|
|
130
131
|
if (!flowSchema.outputTopic) continue;
|
|
131
132
|
|
|
133
|
+
const topicOut = upperCase(flowSchema.flowTag);
|
|
134
|
+
if (processedTopics.has(topicOut)) continue;
|
|
135
|
+
processedTopics.add(topicOut);
|
|
136
|
+
|
|
132
137
|
await appendRenderedTemplate(
|
|
133
138
|
path.join(wsTemplatesDir, 'ProcessWebSocketSub_Yaml.ejs'),
|
|
134
139
|
resourceYamlPath,
|
|
135
140
|
{
|
|
136
|
-
subscribeTo:
|
|
141
|
+
subscribeTo: topicOut,
|
|
137
142
|
queueName: completeFunctionName,
|
|
138
143
|
upperQueueName: upperCase(completeFunctionName)
|
|
139
144
|
}
|
|
@@ -127,6 +127,7 @@ export async function generateFlowResourceYaml(allSchemas, options) {
|
|
|
127
127
|
|
|
128
128
|
const systemOwnTopicFlows = [
|
|
129
129
|
'ProcessLogical',
|
|
130
|
+
'PaginateProcessLogical',
|
|
130
131
|
'FindData',
|
|
131
132
|
'Create',
|
|
132
133
|
'Update',
|
|
@@ -169,7 +170,6 @@ export async function generateFlowResourceYaml(allSchemas, options) {
|
|
|
169
170
|
const systemQueues = [
|
|
170
171
|
'Register',
|
|
171
172
|
'WebSocket',
|
|
172
|
-
'PaginateProcessLogical',
|
|
173
173
|
'CreateComplete',
|
|
174
174
|
'UpdateComplete',
|
|
175
175
|
'DeleteComplete',
|
package/src/generate.js
CHANGED
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
Copyright (C) 2020 Sven Mason <http://izara.io>
|
|
3
3
|
|
|
4
4
|
This program is free software: you can redistribute it and/or modify
|
|
5
|
-
it under the terms of the GNU Affero General Public License as
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
it under the terms of the GNU Affero General Public License as published by
|
|
6
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
(at your option) any later version.
|
|
8
8
|
|
|
9
9
|
This program is distributed in the hope that it will be useful,
|
|
10
10
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
-
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
11
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
12
|
GNU Affero General Public License for more details.
|
|
13
13
|
|
|
14
14
|
You should have received a copy of the GNU Affero General Public License
|
|
15
|
-
along with this program.
|
|
15
|
+
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { join } from 'path';
|
|
@@ -24,53 +24,51 @@ import { generateSchema } from './generateSchema.js';
|
|
|
24
24
|
const { validateLocalSchema } = validator;
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
* High-level
|
|
28
|
-
*
|
|
27
|
+
* High-level API to validate schemas and generate code.
|
|
28
|
+
* Wires legacy Logger and validateLocalSchema.
|
|
29
29
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* @param {string}
|
|
34
|
-
* @param {
|
|
35
|
-
* @param {
|
|
36
|
-
* @param {
|
|
37
|
-
* @param {boolean} [options.
|
|
38
|
-
* @param {
|
|
39
|
-
* @
|
|
40
|
-
* @returns {Promise<{ durationMs: number }>}
|
|
30
|
+
* @param {string} rootPath - The root path of the project.
|
|
31
|
+
* @param {object} [options={}] - Configuration options.
|
|
32
|
+
* @param {object} [options.logger] - Logger instance.
|
|
33
|
+
* @param {string} [options.configPath] - Path to the serverless configuration file.
|
|
34
|
+
* @param {string} [options.outputPath] - Output directory path.
|
|
35
|
+
* @param {boolean} [options.skipValidation=false] - Whether to skip schema validation.
|
|
36
|
+
* @param {boolean} [options.skipGenerateSchema=false] - Whether to skip schema generation.
|
|
37
|
+
* @param {boolean} [options.skipGenerateCode=false] - Whether to skip code generation.
|
|
38
|
+
* @param {Array} [options.schemaTemplateConfig] - Schema template configuration.
|
|
39
|
+
* @returns {Promise<{durationMs: number}>} Generation duration details.
|
|
41
40
|
*/
|
|
42
41
|
async function generate(rootPath, options = {}) {
|
|
43
42
|
const {
|
|
44
43
|
logger = createSilentLogger(),
|
|
45
44
|
configPath = join(rootPath, 'config', 'serverless.config.yml'),
|
|
45
|
+
outputPath = rootPath,
|
|
46
46
|
skipValidation = false,
|
|
47
47
|
skipGenerateSchema = false,
|
|
48
|
-
|
|
48
|
+
skipGenerateCode = false,
|
|
49
|
+
schemaTemplateConfig
|
|
49
50
|
} = options;
|
|
50
51
|
|
|
51
52
|
const schemasPath = join(rootPath, 'app', 'src', 'schemas');
|
|
52
53
|
|
|
53
|
-
// Auto-generate dynamic schemas before validation (unless skipped)
|
|
54
54
|
if (!skipGenerateSchema) {
|
|
55
|
-
await generateSchema(rootPath, {
|
|
55
|
+
await generateSchema(rootPath, {
|
|
56
|
+
configPath,
|
|
57
|
+
outputPath,
|
|
58
|
+
schemaTemplateConfig
|
|
59
|
+
});
|
|
56
60
|
}
|
|
57
61
|
|
|
58
|
-
// Validate schemas before generating (unless skipped)
|
|
59
62
|
if (!skipValidation) {
|
|
60
63
|
patchFlowStepsValidationSchema();
|
|
61
64
|
|
|
62
|
-
console.info('[INFO] [generate]
|
|
65
|
+
console.info('[INFO] [generate] STATUS=VALIDATING | local schemas...');
|
|
63
66
|
const validateStartTime = Date.now();
|
|
64
|
-
|
|
65
|
-
const result = await validateLocalSchema(
|
|
66
|
-
{ logger },
|
|
67
|
-
schemasPath,
|
|
68
|
-
configPath
|
|
69
|
-
);
|
|
70
|
-
|
|
67
|
+
const result = await validateLocalSchema({ logger }, schemasPath, configPath);
|
|
71
68
|
const validateDuration = Date.now() - validateStartTime;
|
|
69
|
+
|
|
72
70
|
console.info(
|
|
73
|
-
`[INFO] [generate]
|
|
71
|
+
`[INFO] [generate] STATUS=VALIDATED | errors=${result.validateErrors.length} | elapsedMs=${validateDuration}`
|
|
74
72
|
);
|
|
75
73
|
|
|
76
74
|
if (result.validateErrors.length > 0) {
|
|
@@ -84,20 +82,26 @@ async function generate(rootPath, options = {}) {
|
|
|
84
82
|
}
|
|
85
83
|
}
|
|
86
84
|
|
|
85
|
+
if (skipGenerateCode) {
|
|
86
|
+
return { durationMs: 0 };
|
|
87
|
+
}
|
|
88
|
+
|
|
87
89
|
const startTime = Date.now();
|
|
88
90
|
await generateCode(rootPath, { outputPath });
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return { durationMs };
|
|
91
|
+
return { durationMs: Date.now() - startTime };
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
function patchFlowStepsValidationSchema() {
|
|
95
95
|
const flowStepsSchema = basicValidatorSchema.basicFlowSchema?.properties?.flowSteps;
|
|
96
|
+
if (!flowStepsSchema || flowStepsSchema.oneOf) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
96
99
|
|
|
97
|
-
if (!flowStepsSchema || flowStepsSchema.oneOf) return;
|
|
98
100
|
const objectStepSchema = {
|
|
99
101
|
...flowStepsSchema.items,
|
|
100
|
-
required: (flowStepsSchema.items.required || []).filter(
|
|
102
|
+
required: (flowStepsSchema.items.required || []).filter(
|
|
103
|
+
item => item !== 'stepName'
|
|
104
|
+
)
|
|
101
105
|
};
|
|
102
106
|
|
|
103
107
|
basicValidatorSchema.basicFlowSchema.properties.flowSteps = {
|
|
@@ -114,10 +118,6 @@ function patchFlowStepsValidationSchema() {
|
|
|
114
118
|
};
|
|
115
119
|
}
|
|
116
120
|
|
|
117
|
-
/**
|
|
118
|
-
* Creates a silent logger that only outputs errors.
|
|
119
|
-
* @returns {Object} Logger interface
|
|
120
|
-
*/
|
|
121
121
|
function createSilentLogger() {
|
|
122
122
|
return {
|
|
123
123
|
debug: () => {},
|
package/src/generateCode.js
CHANGED
|
@@ -37,6 +37,14 @@ function createGeneratorOptions(rootPath, options = {}) {
|
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Low-level generator to generate application code files from all schemas.
|
|
42
|
+
*
|
|
43
|
+
* @param {string} rootPath - The root path of the project.
|
|
44
|
+
* @param {object} [options={}] - Configuration options.
|
|
45
|
+
* @param {string} [options.outputPath] - Output directory path.
|
|
46
|
+
* @returns {Promise<void>}
|
|
47
|
+
*/
|
|
40
48
|
export async function generateCode(rootPath, options = {}) {
|
|
41
49
|
console.log(
|
|
42
50
|
'[INFO] [generateCode] STATUS=STARTING | Generating code files from all schemas...'
|
|
@@ -130,6 +138,57 @@ export async function generateCode(rootPath, options = {}) {
|
|
|
130
138
|
);
|
|
131
139
|
collectHookBackups(outputPath);
|
|
132
140
|
|
|
141
|
+
function processSourceHooks(currentDir) {
|
|
142
|
+
if (!fs.existsSync(currentDir)) return;
|
|
143
|
+
|
|
144
|
+
const items = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
145
|
+
const hasSource = items.some(
|
|
146
|
+
item => item.isDirectory() && item.name === 'source'
|
|
147
|
+
);
|
|
148
|
+
const hasHook = items.some(
|
|
149
|
+
item => item.isDirectory() && item.name === 'hook'
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
if (hasSource && hasHook) {
|
|
153
|
+
const sourcePath = path.join(currentDir, 'source');
|
|
154
|
+
const hookPath = path.join(currentDir, 'hook');
|
|
155
|
+
const hookItems = fs.readdirSync(hookPath, { withFileTypes: true });
|
|
156
|
+
|
|
157
|
+
for (const hookItem of hookItems) {
|
|
158
|
+
if (!hookItem.isFile()) continue;
|
|
159
|
+
|
|
160
|
+
const hookFile = path.join(hookPath, hookItem.name);
|
|
161
|
+
const sourceFile = path.join(sourcePath, hookItem.name);
|
|
162
|
+
|
|
163
|
+
if (!fs.existsSync(sourceFile)) {
|
|
164
|
+
fs.copyFileSync(hookFile, sourceFile);
|
|
165
|
+
console.log(
|
|
166
|
+
`[INFO] [generateCode] HOOK APPLIED: Created ${hookItem.name} from hook.`
|
|
167
|
+
);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const mergedContent = mergeHookIntoSource(
|
|
172
|
+
fs.readFileSync(sourceFile, 'utf8'),
|
|
173
|
+
fs.readFileSync(hookFile, 'utf8'),
|
|
174
|
+
sourceFile
|
|
175
|
+
);
|
|
176
|
+
fs.writeFileSync(sourceFile, mergedContent);
|
|
177
|
+
console.log(
|
|
178
|
+
`[INFO] [generateCode] HOOK APPLIED: Merged ${hookItem.name} into source.`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
} else if (hasSource) {
|
|
182
|
+
fs.mkdirSync(path.join(currentDir, 'hook'), { recursive: true });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
for (const item of items) {
|
|
186
|
+
if (!item.isDirectory() || item.name === 'source' || item.name === 'hook')
|
|
187
|
+
continue;
|
|
188
|
+
processSourceHooks(path.join(currentDir, item.name));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
133
192
|
// ponytail: ensure hooks are restored even if generation fails
|
|
134
193
|
try {
|
|
135
194
|
console.log(
|
|
@@ -204,80 +263,30 @@ export async function generateCode(rootPath, options = {}) {
|
|
|
204
263
|
await generateSharedResourceYaml(allSchemas, generatorOptions);
|
|
205
264
|
await generateInitialSetup(allSchemas, generatorOptions);
|
|
206
265
|
|
|
207
|
-
// 3. Hook Integration
|
|
208
|
-
console.log(
|
|
209
|
-
'[INFO] [generateCode] STATUS=HOOKS | Processing hook folders beside source folders...'
|
|
210
|
-
);
|
|
211
|
-
|
|
212
|
-
function processSourceHooks(currentDir) {
|
|
213
|
-
if (!fs.existsSync(currentDir)) return;
|
|
214
|
-
|
|
215
|
-
const items = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
216
|
-
const hasSource = items.some(
|
|
217
|
-
item => item.isDirectory() && item.name === 'source'
|
|
218
|
-
);
|
|
219
|
-
const hasHook = items.some(
|
|
220
|
-
item => item.isDirectory() && item.name === 'hook'
|
|
221
|
-
);
|
|
222
|
-
|
|
223
|
-
if (hasSource && hasHook) {
|
|
224
|
-
const sourcePath = path.join(currentDir, 'source');
|
|
225
|
-
const hookPath = path.join(currentDir, 'hook');
|
|
226
|
-
const hookItems = fs.readdirSync(hookPath, { withFileTypes: true });
|
|
227
266
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const mergedContent = mergeHookIntoSource(
|
|
243
|
-
fs.readFileSync(sourceFile, 'utf8'),
|
|
244
|
-
fs.readFileSync(hookFile, 'utf8'),
|
|
245
|
-
sourceFile
|
|
246
|
-
);
|
|
247
|
-
fs.writeFileSync(sourceFile, mergedContent);
|
|
248
|
-
console.log(
|
|
249
|
-
`[INFO] [generateCode] HOOK APPLIED: Merged ${hookItem.name} into source.`
|
|
250
|
-
);
|
|
267
|
+
} finally {
|
|
268
|
+
// Restore backed-up hooks
|
|
269
|
+
if (hookDirs.size > 0 || hookBackups.length > 0) {
|
|
270
|
+
console.log(
|
|
271
|
+
`[INFO] [generateCode] STATUS=RESTORE | Restoring ${hookBackups.length} hook files...`
|
|
272
|
+
);
|
|
273
|
+
for (const relativeHookDir of hookDirs) {
|
|
274
|
+
fs.mkdirSync(path.join(outputPath, relativeHookDir), { recursive: true });
|
|
275
|
+
}
|
|
276
|
+
for (const backup of hookBackups) {
|
|
277
|
+
const hookDir = path.join(outputPath, backup.relativePath);
|
|
278
|
+
fs.writeFileSync(path.join(hookDir, backup.name), backup.content);
|
|
251
279
|
}
|
|
252
|
-
} else if (hasSource) {
|
|
253
|
-
fs.mkdirSync(path.join(currentDir, 'hook'), { recursive: true });
|
|
254
280
|
}
|
|
255
281
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
}
|
|
261
|
-
}
|
|
282
|
+
console.log(
|
|
283
|
+
'[INFO] [generateCode] STATUS=HOOKS | Processing hook folders beside source folders...'
|
|
284
|
+
);
|
|
285
|
+
processSourceHooks(generatorOptions.outputPath);
|
|
262
286
|
|
|
263
|
-
} finally {
|
|
264
|
-
// Restore backed-up hooks
|
|
265
|
-
if (hookDirs.size > 0 || hookBackups.length > 0) {
|
|
266
287
|
console.log(
|
|
267
|
-
|
|
288
|
+
'[INFO] [generateCode] STATUS=FINISHED | Code generation complete.'
|
|
268
289
|
);
|
|
269
|
-
for (const relativeHookDir of hookDirs) {
|
|
270
|
-
fs.mkdirSync(path.join(outputPath, relativeHookDir), { recursive: true });
|
|
271
|
-
}
|
|
272
|
-
for (const backup of hookBackups) {
|
|
273
|
-
const hookDir = path.join(outputPath, backup.relativePath);
|
|
274
|
-
fs.writeFileSync(path.join(hookDir, backup.name), backup.content);
|
|
275
|
-
}
|
|
276
290
|
}
|
|
277
|
-
|
|
278
|
-
processSourceHooks(generatorOptions.outputPath);
|
|
279
|
-
|
|
280
|
-
console.log(
|
|
281
|
-
'[INFO] [generateCode] STATUS=FINISHED | Code generation complete.'
|
|
282
|
-
);
|
|
283
291
|
}
|
|
292
|
+
|
package/src/generateIntTest.js
CHANGED
|
@@ -20,7 +20,8 @@ import ejs from 'ejs';
|
|
|
20
20
|
import beautify from 'js-beautify/js/index.js';
|
|
21
21
|
|
|
22
22
|
import generateTestTemplate from './generateResources/IntTest/generateTests/generateTemplate.js';
|
|
23
|
-
import {
|
|
23
|
+
import { DEFAULT_BUCKET, uploadToS3 } from './generateResources/IntTest/upload/uploadIntTest.js';
|
|
24
|
+
import { getIntTestOutputPath, getServiceTag, runTemplateItems } from './intTestGeneratorShared.js';
|
|
24
25
|
|
|
25
26
|
const BEAUTIFY_SETTING = {
|
|
26
27
|
indent_size: '2',
|
|
@@ -105,10 +106,26 @@ async function writeSource({ templatePath, templateData, setting }, { checkCreat
|
|
|
105
106
|
return [true, {}];
|
|
106
107
|
}
|
|
107
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Result-Driven generator to generate Integration Tests.
|
|
111
|
+
* Generates local files and can upload generated JSON payloads to S3.
|
|
112
|
+
*
|
|
113
|
+
* @param {string} rootPath - The root path of the project.
|
|
114
|
+
* @param {object} [options={}] - Configuration options.
|
|
115
|
+
* @param {string} [options.outputPath] - Output directory path.
|
|
116
|
+
* @param {string|null} [options.s3Prefix=null] - S3 key prefix for integration test resources.
|
|
117
|
+
* @param {boolean} [options.uploadToS3=false] - Upload generated IntTest JSON files to S3.
|
|
118
|
+
* @param {string} [options.s3Bucket='integrationtest-config-us-east-2'] - Target S3 bucket.
|
|
119
|
+
* @param {object} [options._izContext] - Integration context containing logger interface.
|
|
120
|
+
* @returns {Promise<{serviceTag: string, count: number, durationMs: number, uploadedCount: number, s3Bucket: string|null}>}
|
|
121
|
+
*/
|
|
108
122
|
export async function generateIntTest(rootPath, options = {}) {
|
|
109
123
|
const {
|
|
110
124
|
outputPath = rootPath,
|
|
111
125
|
s3Prefix = null,
|
|
126
|
+
// ponytail: default uploadToS3 to false because generate-Resources uploads the files.
|
|
127
|
+
uploadToS3: shouldUploadToS3 = false,
|
|
128
|
+
s3Bucket = DEFAULT_BUCKET,
|
|
112
129
|
_izContext = {
|
|
113
130
|
logger: {
|
|
114
131
|
debug: () => {},
|
|
@@ -125,7 +142,7 @@ export async function generateIntTest(rootPath, options = {}) {
|
|
|
125
142
|
throw new Error('Missing iz_serviceTag in serverless.config.yml — required IntTest generation');
|
|
126
143
|
}
|
|
127
144
|
|
|
128
|
-
const savePath =
|
|
145
|
+
const savePath = getIntTestOutputPath(outputPath, rootPath);
|
|
129
146
|
const items = await generateTestTemplate(_izContext, savePath, {
|
|
130
147
|
serviceTag,
|
|
131
148
|
rootPath,
|
|
@@ -137,6 +154,21 @@ export async function generateIntTest(rootPath, options = {}) {
|
|
|
137
154
|
errorPrefix: 'IntTest source'
|
|
138
155
|
});
|
|
139
156
|
|
|
157
|
+
let uploadedKeys = [];
|
|
158
|
+
if (shouldUploadToS3) {
|
|
159
|
+
uploadedKeys = await uploadToS3(join(savePath, serviceTag), {
|
|
160
|
+
serviceTag,
|
|
161
|
+
s3Prefix,
|
|
162
|
+
bucket: s3Bucket
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
140
166
|
const durationMs = Date.now() - startTime;
|
|
141
|
-
return {
|
|
167
|
+
return {
|
|
168
|
+
serviceTag,
|
|
169
|
+
count: items.length,
|
|
170
|
+
durationMs,
|
|
171
|
+
uploadedCount: uploadedKeys.length,
|
|
172
|
+
s3Bucket: shouldUploadToS3 ? s3Bucket : null
|
|
173
|
+
};
|
|
142
174
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/* Copyright (C) 2020 Sven Mason <http://izara.io>
|
|
2
2
|
|
|
3
3
|
This program is free software: you can redistribute it and/or modify
|
|
4
|
-
it under the terms of the GNU Affero General Public License as
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
it under the terms of the GNU Affero General Public License as published by
|
|
5
|
+
Free Software Foundation, either version 3 of the License, or (at your
|
|
6
|
+
option) any later version.
|
|
7
7
|
|
|
8
8
|
This program is distributed in the hope that it will be useful,
|
|
9
9
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
@@ -19,7 +19,8 @@ import { join } from 'path';
|
|
|
19
19
|
import ejs from 'ejs';
|
|
20
20
|
|
|
21
21
|
import generateResourceTemplate from './generateResources/IntTest/generateResources/generateResources.js';
|
|
22
|
-
import {
|
|
22
|
+
import { DEFAULT_BUCKET, uploadToS3 } from './generateResources/IntTest/upload/uploadIntTest.js';
|
|
23
|
+
import { getIntTestOutputPath, getServiceTag, runTemplateItems } from './intTestGeneratorShared.js';
|
|
23
24
|
|
|
24
25
|
function writeSource({ templatePath, templateData, setting }, { checkCreateSourcePass = false } = {}) {
|
|
25
26
|
if (!templatePath || !templateData || !setting) {
|
|
@@ -45,9 +46,25 @@ function writeSource({ templatePath, templateData, setting }, { checkCreateSourc
|
|
|
45
46
|
return [true, {}];
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Result-Driven generator to generate Integration Test resources.
|
|
51
|
+
* Generates local files and can upload generated resource payloads to S3.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} rootPath - The root path of the project.
|
|
54
|
+
* @param {object} [options={}] - Configuration options.
|
|
55
|
+
* @param {string} [options.outputPath] - Output directory path.
|
|
56
|
+
* @param {string|null} [options.s3Prefix=null] - S3 key prefix for integration test resources.
|
|
57
|
+
* @param {boolean} [options.uploadToS3=Boolean(options.s3Prefix)] - Upload generated IntTest resource JSON files to S3.
|
|
58
|
+
* @param {string} [options.s3Bucket='integrationtest-config-us-east-2'] - Target S3 bucket.
|
|
59
|
+
* @param {object} [options._izContext] - Integration context containing logger interface.
|
|
60
|
+
* @returns {Promise<{serviceTag: string, count: number, durationMs: number, uploadedCount: number, s3Bucket: string|null}>}
|
|
61
|
+
*/
|
|
48
62
|
export async function generateResourceIntTest(rootPath, options = {}) {
|
|
49
63
|
const {
|
|
50
64
|
outputPath = rootPath,
|
|
65
|
+
s3Prefix = null,
|
|
66
|
+
uploadToS3: shouldUploadToS3 = Boolean(s3Prefix),
|
|
67
|
+
s3Bucket = DEFAULT_BUCKET,
|
|
51
68
|
_izContext = {
|
|
52
69
|
logger: {
|
|
53
70
|
debug: () => {},
|
|
@@ -64,7 +81,7 @@ export async function generateResourceIntTest(rootPath, options = {}) {
|
|
|
64
81
|
throw new Error('Missing iz_serviceTag in serverless.config.yml — required IntTest resource generation');
|
|
65
82
|
}
|
|
66
83
|
|
|
67
|
-
const savePath =
|
|
84
|
+
const savePath = getIntTestOutputPath(outputPath, rootPath);
|
|
68
85
|
const items = await generateResourceTemplate(_izContext, savePath, { serviceTag });
|
|
69
86
|
|
|
70
87
|
await runTemplateItems(items, {
|
|
@@ -72,6 +89,21 @@ export async function generateResourceIntTest(rootPath, options = {}) {
|
|
|
72
89
|
errorPrefix: 'IntTest resource'
|
|
73
90
|
});
|
|
74
91
|
|
|
92
|
+
let uploadedKeys = [];
|
|
93
|
+
if (shouldUploadToS3) {
|
|
94
|
+
uploadedKeys = await uploadToS3(join(savePath, serviceTag), {
|
|
95
|
+
serviceTag,
|
|
96
|
+
s3Prefix,
|
|
97
|
+
bucket: s3Bucket
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
75
101
|
const durationMs = Date.now() - startTime;
|
|
76
|
-
return {
|
|
102
|
+
return {
|
|
103
|
+
serviceTag,
|
|
104
|
+
count: items.length,
|
|
105
|
+
durationMs,
|
|
106
|
+
uploadedCount: uploadedKeys.length,
|
|
107
|
+
s3Bucket: shouldUploadToS3 ? s3Bucket : null
|
|
108
|
+
};
|
|
77
109
|
}
|
|
@@ -44,16 +44,30 @@ async function generateCodeWithTemplate(_izContext, savePath, settings) {
|
|
|
44
44
|
let allIntTestConfig = await getIntTestConfig(
|
|
45
45
|
_izContext,
|
|
46
46
|
'generateIntegrationTest/libs/',
|
|
47
|
-
rootPath
|
|
47
|
+
rootPath || savePath
|
|
48
|
+
);
|
|
49
|
+
const event = await generateEvent(
|
|
50
|
+
_izContext,
|
|
51
|
+
allIntTestConfig,
|
|
52
|
+
savePath,
|
|
53
|
+
serviceTag
|
|
54
|
+
);
|
|
55
|
+
const test = await generateTest(
|
|
56
|
+
_izContext,
|
|
57
|
+
allIntTestConfig,
|
|
58
|
+
savePath,
|
|
59
|
+
serviceTag
|
|
48
60
|
);
|
|
49
|
-
const event = await generateEvent(_izContext, allIntTestConfig, savePath, serviceTag);
|
|
50
|
-
const test = await generateTest(_izContext, allIntTestConfig, savePath, serviceTag);
|
|
51
61
|
const intTestBucketPath = await generatePathIntTest(_izContext, savePath, {
|
|
52
62
|
serviceTag,
|
|
53
63
|
s3Prefix
|
|
54
64
|
});
|
|
55
65
|
|
|
56
|
-
allCreateSource.push(
|
|
66
|
+
allCreateSource.push(
|
|
67
|
+
...[event, test, intTestBucketPath]
|
|
68
|
+
.flat()
|
|
69
|
+
.filter(item => !(Array.isArray(item) && item.length === 0))
|
|
70
|
+
);
|
|
57
71
|
|
|
58
72
|
return allCreateSource;
|
|
59
73
|
} catch (error) {
|
|
@@ -1,65 +1,53 @@
|
|
|
1
|
-
/*
|
|
2
|
-
Copyright (C) 2020 Sven Mason <http://izara.io>
|
|
1
|
+
/* Copyright (C) 2020 Sven Mason <http://izara.io>
|
|
3
2
|
|
|
4
3
|
This program is free software: you can redistribute it and/or modify
|
|
5
|
-
|
|
4
|
+
under the terms of the GNU Affero General Public License as
|
|
6
5
|
published by the Free Software Foundation, either version 3 of the
|
|
7
6
|
License, or (at your option) any later version.
|
|
8
7
|
|
|
9
8
|
This program is distributed in the hope that it will be useful,
|
|
10
9
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
-
MERCHANTABILITY or FITNESS FOR
|
|
10
|
+
MERCHANTABILITY or FITNESS FOR PARTICULAR PURPOSE. See the
|
|
12
11
|
GNU Affero General Public License for more details.
|
|
13
12
|
|
|
14
13
|
You should have received a copy of the GNU Affero General Public License
|
|
15
|
-
along with this program.
|
|
14
|
+
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
16
15
|
*/
|
|
17
|
-
|
|
18
|
-
import path, { join } from 'path';
|
|
16
|
+
import path from 'path';
|
|
19
17
|
import { fileURLToPath } from 'url';
|
|
18
|
+
|
|
19
|
+
import { bucketPathForUpload } from '../../upload/uploadIntTest.js';
|
|
20
|
+
|
|
20
21
|
const __filename = fileURLToPath(import.meta.url);
|
|
21
22
|
const __dirname = path.dirname(__filename);
|
|
22
23
|
const templatePath = path.join(__dirname, './template.ejs');
|
|
23
|
-
|
|
24
|
-
// Refactored: legacy `bucketPathForUpload` (S3-based) was removed with IntTest/upload/.
|
|
25
|
-
// The new flow doesn't sync IntTest configs to S3 — return empty array as no-op.
|
|
26
|
-
async function bucketPathForUpload(_srcPath, _settings) {
|
|
27
|
-
return [];
|
|
28
|
-
}
|
|
24
|
+
const INT_TEST_SUFFIX_PATTERN = /(events|tests|resources|dynamoDBSeedDataTags|graphSeedDataTags)\.json$/;
|
|
29
25
|
|
|
30
26
|
async function data(_izContext, srcPath, settings) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
let pathIntTestInBuckets = await bucketPathForUpload(
|
|
35
|
-
join(srcPath, `../../src/generateIntegrationTest/${settings.serviceTag}`),
|
|
27
|
+
const pathIntTestInBuckets = await bucketPathForUpload(
|
|
28
|
+
path.join(srcPath, settings.serviceTag),
|
|
36
29
|
settings
|
|
37
30
|
);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
31
|
+
const intTestPaths = pathIntTestInBuckets
|
|
32
|
+
.filter(({ key }) => INT_TEST_SUFFIX_PATTERN.test(key))
|
|
33
|
+
.map(({ key }) => key.replace(INT_TEST_SUFFIX_PATTERN, ''));
|
|
42
34
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
let path = pathIntTestInBucket.key.replace(/(events|tests|resources|dynamoDBSeedDataTags|graphSeedDataTags).json$/,);
|
|
46
|
-
intTestPaths.push(`${path}`);
|
|
47
|
-
}
|
|
35
|
+
if (!intTestPaths.length) {
|
|
36
|
+
return [];
|
|
48
37
|
}
|
|
49
38
|
|
|
50
39
|
return {
|
|
51
40
|
templateData: { intTestPaths },
|
|
52
|
-
templatePath
|
|
41
|
+
templatePath,
|
|
53
42
|
setting: {
|
|
54
43
|
generateHookFile: false,
|
|
55
|
-
savePath: path.join(srcPath,
|
|
44
|
+
savePath: path.join(srcPath, './libs/'),
|
|
56
45
|
saveFileName: 'IntTestPath',
|
|
57
46
|
fileExtension: '.js',
|
|
58
|
-
|
|
59
|
-
|
|
47
|
+
isAppend: false,
|
|
48
|
+
cleanSavePath: false
|
|
60
49
|
}
|
|
61
50
|
};
|
|
62
51
|
}
|
|
63
52
|
|
|
64
|
-
|
|
65
53
|
export default data;
|
|
@@ -393,10 +393,18 @@ function createResourceName(eventTag) {
|
|
|
393
393
|
}
|
|
394
394
|
|
|
395
395
|
async function getIntTestConfig(_izContext, intTestPath, rootPath) {
|
|
396
|
+
// ponytail: find first existing integration test libs folder or fall back to standard path
|
|
397
|
+
const targetLibsPath = rootPath.includes('generateIntegrationTest')
|
|
398
|
+
? path.join(rootPath.split('generateIntegrationTest')[0], 'generateIntegrationTest', 'libs')
|
|
399
|
+
: [
|
|
400
|
+
path.join(rootPath, 'app/src/generateIntegrationTest/libs'),
|
|
401
|
+
path.join(rootPath, 'src/generateIntegrationTest/libs')
|
|
402
|
+
].find(fs.existsSync) || path.join(rootPath, 'src/generateIntegrationTest/libs');
|
|
403
|
+
|
|
396
404
|
return await getObjectSchema.getDataFromPath(
|
|
397
405
|
_izContext,
|
|
398
|
-
|
|
399
|
-
path.
|
|
406
|
+
path.basename(targetLibsPath),
|
|
407
|
+
path.dirname(targetLibsPath),
|
|
400
408
|
{ exceptFileName: 'saveConfig' }
|
|
401
409
|
);
|
|
402
410
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/* Copyright (C) 2020 Sven Mason <http://izara.io>
|
|
2
|
+
|
|
3
|
+
This program is free software: you can redistribute it and/or modify
|
|
4
|
+
under the terms of the GNU Affero General Public License as
|
|
5
|
+
published by the Free Software Foundation, either version 3 of the
|
|
6
|
+
License, or (at your option) any later version.
|
|
7
|
+
|
|
8
|
+
This program is distributed in the hope that it will be useful,
|
|
9
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
10
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
11
|
+
GNU Affero General Public License for more details.
|
|
12
|
+
|
|
13
|
+
You should have received a copy of the GNU Affero General Public License
|
|
14
|
+
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
15
|
+
*/
|
|
16
|
+
import fs from 'fs';
|
|
17
|
+
import path from 'path';
|
|
18
|
+
|
|
19
|
+
import { s3 } from '@izara_project/izara-core-library-s3';
|
|
20
|
+
import Logger from '@izara_project/izara-core-library-logger';
|
|
21
|
+
|
|
22
|
+
const DEFAULT_BUCKET = 'integrationtest-config-us-east-2';
|
|
23
|
+
const _izContext = { Logger };
|
|
24
|
+
|
|
25
|
+
function getAllFiles(dirPath, filePaths = []) {
|
|
26
|
+
if (!fs.existsSync(dirPath)) return filePaths;
|
|
27
|
+
|
|
28
|
+
for (const file of fs.readdirSync(dirPath)) {
|
|
29
|
+
const fullPath = path.join(dirPath, file);
|
|
30
|
+
const stat = fs.statSync(fullPath);
|
|
31
|
+
|
|
32
|
+
if (stat.isDirectory()) {
|
|
33
|
+
getAllFiles(fullPath, filePaths);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
filePaths.push(fullPath);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return filePaths;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function bucketPathForUpload(folderPath, settings = {}) {
|
|
44
|
+
const dataToUploads = [];
|
|
45
|
+
const files = getAllFiles(folderPath);
|
|
46
|
+
|
|
47
|
+
for (const filePath of files) {
|
|
48
|
+
const fileContent = fs.readFileSync(filePath);
|
|
49
|
+
// ponytail: use indexOf to find first occurrence of serviceTag so nested test case folders containing the serviceTag name do not get cut off.
|
|
50
|
+
const index = settings.serviceTag ? filePath.indexOf(settings.serviceTag) : -1;
|
|
51
|
+
const relativePath = index !== -1
|
|
52
|
+
? filePath.substring(index)
|
|
53
|
+
: path.basename(filePath);
|
|
54
|
+
const s3Key = path
|
|
55
|
+
.join('test_config', settings.s3Prefix || '', relativePath)
|
|
56
|
+
.split(path.sep)
|
|
57
|
+
.join('/');
|
|
58
|
+
|
|
59
|
+
dataToUploads.push({ key: s3Key, fileContent });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return dataToUploads;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function uploadToS3(folderPath, settings = {}) {
|
|
66
|
+
const uploads = await bucketPathForUpload(folderPath, settings);
|
|
67
|
+
const bucket = settings.bucket || DEFAULT_BUCKET;
|
|
68
|
+
|
|
69
|
+
await Promise.all(uploads.map(({ key, fileContent }) => (
|
|
70
|
+
s3.putObjectS3(_izContext, {
|
|
71
|
+
Bucket: bucket,
|
|
72
|
+
Key: key,
|
|
73
|
+
Body: fileContent,
|
|
74
|
+
ContentType: 'application/json'
|
|
75
|
+
})
|
|
76
|
+
)));
|
|
77
|
+
|
|
78
|
+
return uploads.map(({ key }) => key);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export { DEFAULT_BUCKET, bucketPathForUpload, uploadToS3 };
|
|
82
|
+
export default { DEFAULT_BUCKET, bucketPathForUpload, uploadToS3 };
|
package/src/generateSchema.js
CHANGED
|
@@ -15,34 +15,36 @@ import { generatePropertyValueRelationshipSchemas } from './schemaGenerators/app
|
|
|
15
15
|
import { generateAttributeTreeObjectSchemas } from './schemaGenerators/app/src/schemas/ObjectSchemas/AttributeTreeSchemaGenerator.js';
|
|
16
16
|
import { generateAttributeTreeRelationshipSchemas } from './schemaGenerators/app/src/schemas/RelationshipSchemas/AttributeTreeRelationshipSchemaGenerator.js';
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Low-level generator to generate dynamic schemas (FlowSchema, etc.).
|
|
20
|
+
*
|
|
21
|
+
* @param {string} rootPath - The root path of the project.
|
|
22
|
+
* @param {object} [options={}] - Configuration options.
|
|
23
|
+
* @param {string} [options.configPath] - Path to the serverless configuration file.
|
|
24
|
+
* @param {string} [options.outputPath] - Output directory path.
|
|
25
|
+
* @param {Array} [options.schemaTemplateConfig] - Schema template configuration.
|
|
26
|
+
* @returns {Promise<void>}
|
|
27
|
+
*/
|
|
28
|
+
export async function generateSchema(rootPath, options = {}) {
|
|
19
29
|
console.log(
|
|
20
30
|
'[INFO] [generateSchema] STATUS=STARTING | Generating dynamic schemas (FlowSchema, etc.)...'
|
|
21
31
|
);
|
|
22
32
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
try {
|
|
27
|
-
const serviceConfigStr = fs.readFileSync(configPath, 'utf8');
|
|
28
|
-
const serviceConfig = yaml.parse(serviceConfigStr);
|
|
29
|
-
serviceTag = serviceConfig?.main_config?.iz_serviceTag || 'unknownService';
|
|
30
|
-
} catch (e) {
|
|
31
|
-
console.warn(
|
|
32
|
-
`[WARNING] Could not read service config at ${configPath}. Using default serviceTag.`
|
|
33
|
-
);
|
|
34
|
-
}
|
|
33
|
+
const configPath =
|
|
34
|
+
options.configPath || path.join(rootPath, 'config', 'serverless.config.yml');
|
|
35
|
+
const serviceTag = readServiceTag(configPath);
|
|
35
36
|
|
|
36
|
-
// Phase 1 parsing just to get the raw data needed for generation
|
|
37
37
|
const objectData = await parseObjectSchemas(rootPath);
|
|
38
|
+
applySchemaTemplateConfig(objectData, options.schemaTemplateConfig, serviceTag);
|
|
39
|
+
|
|
38
40
|
const flowData = await parseFlowSchemas(rootPath, objectData);
|
|
39
41
|
const relationshipData = await parseRelationshipSchemas(rootPath, objectData);
|
|
40
42
|
|
|
41
|
-
// Call Schema Generators (Phase 1)
|
|
42
43
|
const generatorOptions = {
|
|
43
44
|
outputPath: options.outputPath || rootPath,
|
|
44
45
|
serviceTag
|
|
45
46
|
};
|
|
47
|
+
|
|
46
48
|
await generateSystemTextSchemas(
|
|
47
49
|
objectData,
|
|
48
50
|
relationshipData,
|
|
@@ -56,8 +58,66 @@ export async function generateSchema(rootPath, options) {
|
|
|
56
58
|
await generatePropertyValueRelationshipSchemas(objectData, generatorOptions);
|
|
57
59
|
await generateAttributeTreeObjectSchemas(objectData, generatorOptions);
|
|
58
60
|
await generateAttributeTreeRelationshipSchemas(objectData, generatorOptions);
|
|
61
|
+
}
|
|
59
62
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
function readServiceTag(configPath) {
|
|
64
|
+
try {
|
|
65
|
+
const serviceConfigStr = fs.readFileSync(configPath, 'utf8');
|
|
66
|
+
const serviceConfig = yaml.parse(serviceConfigStr);
|
|
67
|
+
return serviceConfig?.main_config?.iz_serviceTag || 'unknownService';
|
|
68
|
+
} catch {
|
|
69
|
+
console.warn(
|
|
70
|
+
`[WARNING] Could not read service config at ${configPath}. Using default serviceTag.`
|
|
71
|
+
);
|
|
72
|
+
return 'unknownService';
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function applySchemaTemplateConfig(objectData, schemaTemplateConfig, defaultServiceTag) {
|
|
77
|
+
const templates = Array.isArray(schemaTemplateConfig)
|
|
78
|
+
? schemaTemplateConfig
|
|
79
|
+
: [];
|
|
80
|
+
|
|
81
|
+
for (const template of templates) {
|
|
82
|
+
if (template?.generateName !== 'mainAttributeTree') {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const objectType = template?.objType?.objectType;
|
|
87
|
+
const serviceTag = template?.objType?.serviceTag || defaultServiceTag;
|
|
88
|
+
const attributeTag = template?.attributeTag;
|
|
89
|
+
|
|
90
|
+
if (!objectType || !attributeTag) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const objectSchema = objectData.objects.find(
|
|
95
|
+
obj =>
|
|
96
|
+
obj.objectType === objectType &&
|
|
97
|
+
(obj.serviceTag || defaultServiceTag) === serviceTag
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
if (!objectSchema) {
|
|
101
|
+
console.warn(
|
|
102
|
+
`[WARNING] [generateSchema] Template target not found for ${serviceTag}.${objectType}`
|
|
103
|
+
);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
objectSchema.addOnDataStructure ||= [];
|
|
108
|
+
|
|
109
|
+
const hasAttributeTree = objectSchema.addOnDataStructure.some(
|
|
110
|
+
addOn =>
|
|
111
|
+
addOn?.type === 'attributeTree' &&
|
|
112
|
+
addOn?.attributeTreeTag === attributeTag
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
if (!hasAttributeTree) {
|
|
116
|
+
// ponytail: inject the same addon shape the existing generators already understand
|
|
117
|
+
objectSchema.addOnDataStructure.push({
|
|
118
|
+
type: 'attributeTree',
|
|
119
|
+
attributeTreeTag: attributeTag
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
63
123
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync, rmSync } from 'fs';
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, rmSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import yaml from 'yaml';
|
|
4
4
|
|
|
@@ -8,24 +8,54 @@ function getServiceTag(rootPath) {
|
|
|
8
8
|
return config?.main_config?.iz_serviceTag;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
function getIntTestOutputPath(outputPath, projectRootPath = outputPath) {
|
|
12
|
+
return existsSync(join(projectRootPath, 'app'))
|
|
13
|
+
? join(outputPath, 'app', 'src', 'generateIntegrationTest')
|
|
14
|
+
: join(outputPath, 'src', 'generateIntegrationTest');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function clearSavePath(savePath, preserveFileNames = []) {
|
|
18
|
+
if (!savePath || !existsSync(savePath)) return;
|
|
19
|
+
|
|
20
|
+
const preserved = new Set(preserveFileNames);
|
|
21
|
+
for (const entry of readdirSync(savePath)) {
|
|
22
|
+
if (preserved.has(entry)) continue;
|
|
23
|
+
rmSync(join(savePath, entry), { recursive: true, force: true });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
11
27
|
async function runTemplateItems(items, { writeSource, errorPrefix }) {
|
|
28
|
+
const normalizedItems = (items || []).filter(
|
|
29
|
+
item => !(Array.isArray(item) && item.length === 0)
|
|
30
|
+
);
|
|
31
|
+
if (!normalizedItems.length) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
12
35
|
const clearedPaths = new Set();
|
|
13
36
|
|
|
14
|
-
for (const item of
|
|
37
|
+
for (const item of normalizedItems) {
|
|
15
38
|
if (!item || !item.templatePath || !item.templateData || !item.setting) {
|
|
16
|
-
throw new Error(
|
|
39
|
+
throw new Error(
|
|
40
|
+
`Invalid ${errorPrefix} item: missing templatePath/templateData/setting`
|
|
41
|
+
);
|
|
17
42
|
}
|
|
18
43
|
|
|
19
|
-
const [status, error] = await writeSource(item, {
|
|
44
|
+
const [status, error] = await writeSource(item, {
|
|
45
|
+
checkCreateSourcePass: true
|
|
46
|
+
});
|
|
20
47
|
if (!status) {
|
|
21
|
-
throw new Error(
|
|
48
|
+
throw new Error(
|
|
49
|
+
`${errorPrefix} template validation failed: ${error?.message || error}`
|
|
50
|
+
);
|
|
22
51
|
}
|
|
23
52
|
}
|
|
24
53
|
|
|
25
|
-
for (const item of
|
|
54
|
+
for (const item of normalizedItems) {
|
|
26
55
|
const savePath = item.setting?.savePath;
|
|
27
|
-
|
|
28
|
-
|
|
56
|
+
const cleanSavePathEnabled = item.setting?.cleanSavePath !== false;
|
|
57
|
+
if (savePath && cleanSavePathEnabled && !clearedPaths.has(savePath)) {
|
|
58
|
+
clearSavePath(savePath, item.setting?.preserveFileNames || []);
|
|
29
59
|
clearedPaths.add(savePath);
|
|
30
60
|
}
|
|
31
61
|
|
|
@@ -33,4 +63,4 @@ async function runTemplateItems(items, { writeSource, errorPrefix }) {
|
|
|
33
63
|
}
|
|
34
64
|
}
|
|
35
65
|
|
|
36
|
-
export { getServiceTag, runTemplateItems };
|
|
66
|
+
export { getIntTestOutputPath, getServiceTag, runTemplateItems };
|
|
@@ -20,6 +20,8 @@ export async function parseFlowSchemas(rootPath, hydratedObjectData, options = {
|
|
|
20
20
|
const flowsResponse = await getObjectSchema.getAllLocalFlowSchemas(_izContext, schemasPath);
|
|
21
21
|
let flows = flowsResponse.records || [];
|
|
22
22
|
|
|
23
|
+
const seenFlowTags = new Set(flows.map(f => f.flowTag));
|
|
24
|
+
|
|
23
25
|
// Also read dynamic flows from generateCode directory in the output path
|
|
24
26
|
const targetSchemasDir = options.outputPath ? path.join(options.outputPath, 'app', 'src', 'schemas') : schemasPath;
|
|
25
27
|
const dynamicSchemasPath = path.join(targetSchemasDir, 'FlowSchemas', 'generateCode');
|
|
@@ -32,8 +34,12 @@ export async function parseFlowSchemas(rootPath, hydratedObjectData, options = {
|
|
|
32
34
|
const filePath = path.join(dynamicSchemasPath, file);
|
|
33
35
|
const mod = await import(pathToFileURL(filePath).href);
|
|
34
36
|
if (mod.default) {
|
|
37
|
+
if (seenFlowTags.has(mod.default.flowTag)) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
35
40
|
console.log(`[flowSchemaParser] Loaded dynamic flow: ${mod.default.flowTag}`);
|
|
36
41
|
flows.push(mod.default);
|
|
42
|
+
seenFlowTags.add(mod.default.flowTag);
|
|
37
43
|
} else {
|
|
38
44
|
console.log(`[flowSchemaParser] No default export in ${file}`);
|
|
39
45
|
}
|