@izara_project/izara-core-generate-service-code 1.0.56 → 1.0.58

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@izara_project/izara-core-generate-service-code",
3
- "version": "1.0.56",
3
+ "version": "1.0.58",
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,49 @@
1
+ import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals';
2
+ import path from 'path';
3
+ import fs from 'fs';
4
+ import libs from '../generateResources/IntTest/libs/libs.js';
5
+ import { getObjectSchema } from '@izara_project/izara-core-library-service-schemas';
6
+
7
+ const { getIntTestConfig } = libs;
8
+
9
+ describe('getIntTestConfig path resolution', () => {
10
+ let originalGetDataFromPath;
11
+ let passedArgs;
12
+
13
+ beforeEach(() => {
14
+ originalGetDataFromPath = getObjectSchema.getDataFromPath;
15
+ passedArgs = null;
16
+ getObjectSchema.getDataFromPath = async (_izContext, folderName, rootFolderPath, options) => {
17
+ passedArgs = { folderName, rootFolderPath, options };
18
+ return [];
19
+ };
20
+ });
21
+
22
+ afterEach(() => {
23
+ getObjectSchema.getDataFromPath = originalGetDataFromPath;
24
+ });
25
+
26
+ it('Case 1: should resolve correctly when rootPath includes generateIntegrationTest', async () => {
27
+ const rootPath = '/home/ironfist/Pong/Izara Project/Izara-Market-Product/izara-market-products-sell-offer-price-pong/app/src/generateIntegrationTest/';
28
+ await getIntTestConfig({ logger: console }, 'generateIntegrationTest/libs/', rootPath);
29
+
30
+ expect(passedArgs).not.toBeNull();
31
+ expect(passedArgs.folderName).toBe('libs');
32
+ expect(passedArgs.rootFolderPath).toBe('/home/ironfist/Pong/Izara Project/Izara-Market-Product/izara-market-products-sell-offer-price-pong/app/src/generateIntegrationTest');
33
+ });
34
+
35
+ it('Case 2: should resolve correctly when rootPath is repo root with app/src/... structure', async () => {
36
+ const tmpDir = path.join(process.cwd(), 'src', '__tests__', 'scratch_tmp');
37
+ fs.mkdirSync(path.join(tmpDir, 'app', 'src', 'generateIntegrationTest', 'libs'), { recursive: true });
38
+
39
+ try {
40
+ await getIntTestConfig({ logger: console }, 'generateIntegrationTest/libs/', tmpDir);
41
+
42
+ expect(passedArgs).not.toBeNull();
43
+ expect(passedArgs.folderName).toBe('libs');
44
+ expect(passedArgs.rootFolderPath).toBe(path.join(tmpDir, 'app', 'src', 'generateIntegrationTest'));
45
+ } finally {
46
+ fs.rmSync(tmpDir, { recursive: true, force: true });
47
+ }
48
+ });
49
+ });
@@ -56,8 +56,13 @@ export async function generateRelationshipFlows(allSchemas, options) {
56
56
  console.warn(` [RelationshipFlowGenerator] Warning: Action template not found for ${flowTag}. Skipping.`);
57
57
  }
58
58
 
59
- // 2. Generate Action Step Handlers (based on fixed event ['ownTopic'])
60
- const events = ['ownTopic'];
59
+ // 2. Generate Action Step Handlers
60
+ const flowSchema = allSchemas.flows?.find(f => f.flowTag === flowTag);
61
+ let events = ['ownTopic'];
62
+ if (flowSchema && flowSchema.event) {
63
+ events = Array.isArray(flowSchema.event) ? flowSchema.event : [flowSchema.event];
64
+ }
65
+
61
66
  for (const event of events) {
62
67
  let handlerFileName, handlerContent, handlerFunctionName;
63
68
  if (event === 'ownTopic' || event === 'extTopic') {
@@ -91,6 +96,17 @@ export async function generateRelationshipFlows(allSchemas, options) {
91
96
  });
92
97
  await fs.writeFile(path.join(flowOutputDir, `${handlerFunctionName}.js`), handlerContent, 'utf-8');
93
98
  generatedCount++;
99
+ } else if (event === 'dsq') {
100
+ handlerFunctionName = `${processFunction}_HdrDsq`;
101
+ handlerContent = ejs.render(templates.sqs, {
102
+ flowTag: flowTag,
103
+ flowStepName: '',
104
+ mainFileName: actionFileName,
105
+ functionName: processFunction,
106
+ queueName: `${processFunction}HdrDsq`
107
+ });
108
+ await fs.writeFile(path.join(flowOutputDir, `${handlerFunctionName}.js`), handlerContent, 'utf-8');
109
+ generatedCount++;
94
110
  }
95
111
  }
96
112
 
@@ -81,7 +81,7 @@ export default async function createRelationship(
81
81
  relType,
82
82
  relationshipDirection,
83
83
  relationshipProperties,
84
- deepPathConditional
84
+ deepPathConditional,
85
85
  settings
86
86
  //(<requestParamCreateRel>)
87
87
  //(</requestParamCreateRel>)
@@ -22,7 +22,7 @@ export async function generateWebSocket(allLocalFlowSchemas, appPath, outputBase
22
22
  return;
23
23
  }
24
24
 
25
- console.log(' [WebSocketGenerator] Generated WebSocket files with ProcessWebSocketMain_Main convention.');
25
+ console.log(' [WebSocketGenerator] Generated WebSocket files with WebSocketComplete_Main convention.');
26
26
  const templatesDir = path.join(__dirname, 'templates', 'webSocket');
27
27
  const wsDir = path.join(outputBaseDir, 'app', 'src', 'generatedCode', 'SystemFlowSchemas', 'WebSocketMain', 'source');
28
28
  await fs.mkdir(wsDir, { recursive: true });
@@ -79,8 +79,8 @@ export async function generateWebSocket(allLocalFlowSchemas, appPath, outputBase
79
79
  upperFunctionName: upperCase(wsConnectName)
80
80
  });
81
81
 
82
- // 3. ProcessWebSocketMain
83
- const processFunctionName = 'ProcessWebSocketMain';
82
+ // 3. WebSocketComplete
83
+ const processFunctionName = 'WebSocketComplete';
84
84
  const processHandlerType = 'HdrSqs';
85
85
  const queueName = processFunctionName + shortNameHandler(processHandlerType);
86
86
 
@@ -114,5 +114,5 @@ export async function generateWebSocket(allLocalFlowSchemas, appPath, outputBase
114
114
  }
115
115
  }
116
116
 
117
- console.log(` [WebSocketGenerator] Generated WebSocket files with ProcessWebSocketMain_Main convention.`);
117
+ console.log(` [WebSocketGenerator] Generated WebSocket files with WebSocketComplete_Main convention.`);
118
118
  }
@@ -1,4 +1,4 @@
1
- const ALLOWED_MAIN_EVENTS = ['ownTopic', 'extTopic', 'lambdaSyncInv', 'lambdaSyncApi', 'eventBridge', 's3'];
1
+ const ALLOWED_MAIN_EVENTS = ['ownTopic', 'extTopic', 'lambdaSyncInv', 'lambdaSyncApi', 'eventBridge', 's3', 'dsq'];
2
2
  const MAIN_ASYNC_EVENTS = ['ownTopic', 'extTopic', 's3'];
3
3
  const ALLOWED_STEP_EVENTS = ['sqs', 'ownTopic', 'extTopic', 'dsq', 'lambdaSyncInv', 'lambdaSyncApi'];
4
4
  const RESERVED_SYSTEM_FLOW_TAGS = new Set([
@@ -49,7 +49,6 @@ export async function generateFlowOut(allSchemas, options) {
49
49
 
50
50
  const systemTopics = [
51
51
  'ProcessLogical',
52
- 'PaginateProcessLogical',
53
52
  'FindData',
54
53
  'Create',
55
54
  'Update',
@@ -135,6 +135,7 @@ export async function generateFlowResourceYaml(allSchemas, options) {
135
135
  'CreateRelationship',
136
136
  'UpdateRelationship',
137
137
  'DeleteRelationship',
138
+ 'GetRelationship',
138
139
  'ChangeRelationship',
139
140
  'MoveRelationship',
140
141
  'CreateTargetRole',
@@ -163,6 +164,9 @@ export async function generateFlowResourceYaml(allSchemas, options) {
163
164
  await appendUnique(queueName, resourceSqsYamlTpl, { queueName });
164
165
  }
165
166
 
167
+ // ponytail: add Dsq queue for PaginateProcessLogical
168
+ await appendUnique('PaginateProcessLogicalHdrDsq', resourceSqsYamlTpl, { queueName: 'PaginateProcessLogicalHdrDsq' });
169
+
166
170
  const systemQueues = [
167
171
  'Register',
168
172
  'WebSocket',
@@ -170,7 +174,6 @@ export async function generateFlowResourceYaml(allSchemas, options) {
170
174
  'UpdateComplete',
171
175
  'DeleteComplete',
172
176
  'GetComplete',
173
- 'GetRelationship',
174
177
  'CreateRelationshipComplete',
175
178
  'UpdateRelationshipComplete',
176
179
  'DeleteRelationshipComplete',
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
- published by the Free Software Foundation, either version 3 of the
7
- License, or (at your option) any later version.
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. See the
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. If not, see <http://www.gnu.org/licenses/>.
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 code generation API.
28
- * Consumer only needs to provide rootPath — everything else is auto-resolved.
27
+ * High-level API to validate schemas and generate code.
28
+ * Wires legacy Logger and validateLocalSchema.
29
29
  *
30
- * Wires legacy API surface (Logger + validateLocalSchema) onto the new
31
- * Result-Driven flat-pipeline generators.
32
- *
33
- * @param {string} rootPath - Root path containing /app and /config
34
- * @param {Object} [options]
35
- * @param {Object} [options.logger] - Custom logger (default: silent)
36
- * @param {string} [options.configPath] - Override config path
37
- * @param {boolean} [options.skipValidation] - Skip schema validation (default: false)
38
- * @param {boolean} [options.skipGenerateSchema] - Skip auto-generating dynamic schemas (default: false)
39
- * @param {string} [options.outputPath] - Where to write generated code (default: rootPath)
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
- outputPath = rootPath
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, { outputPath, configPath });
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] STATUS=VALIDATING| local schemas...');
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] STATUS=VALIDATED | errors=${result.validateErrors.length} | elapsedMs=${validateDuration}`
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
- const durationMs = Date.now() - startTime;
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(item => item !== 'stepName')
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: () => {},
@@ -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,9 +138,62 @@ export async function generateCode(rootPath, options = {}) {
130
138
  );
131
139
  collectHookBackups(outputPath);
132
140
 
133
- console.log(
134
- '[INFO] [generateCode] STATUS=CLEANUP | Removing old generatedCode directories...'
135
- );
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
+
192
+ // ponytail: ensure hooks are restored even if generation fails
193
+ try {
194
+ console.log(
195
+ '[INFO] [generateCode] STATUS=CLEANUP | Removing old generatedCode directories...'
196
+ );
136
197
  await fs.promises.rm(generatedCodePath, {
137
198
  recursive: true,
138
199
  force: true
@@ -202,79 +263,30 @@ export async function generateCode(rootPath, options = {}) {
202
263
  await generateSharedResourceYaml(allSchemas, generatorOptions);
203
264
  await generateInitialSetup(allSchemas, generatorOptions);
204
265
 
205
- // 3. Hook Integration
206
- console.log(
207
- '[INFO] [generateCode] STATUS=HOOKS | Processing hook folders beside source folders...'
208
- );
209
-
210
- function processSourceHooks(currentDir) {
211
- if (!fs.existsSync(currentDir)) return;
212
266
 
213
- const items = fs.readdirSync(currentDir, { withFileTypes: true });
214
- const hasSource = items.some(
215
- item => item.isDirectory() && item.name === 'source'
216
- );
217
- const hasHook = items.some(
218
- item => item.isDirectory() && item.name === 'hook'
219
- );
220
-
221
- if (hasSource && hasHook) {
222
- const sourcePath = path.join(currentDir, 'source');
223
- const hookPath = path.join(currentDir, 'hook');
224
- const hookItems = fs.readdirSync(hookPath, { withFileTypes: true });
225
-
226
- for (const hookItem of hookItems) {
227
- if (!hookItem.isFile()) continue;
228
-
229
- const hookFile = path.join(hookPath, hookItem.name);
230
- const sourceFile = path.join(sourcePath, hookItem.name);
231
-
232
- if (!fs.existsSync(sourceFile)) {
233
- fs.copyFileSync(hookFile, sourceFile);
234
- console.log(
235
- `[INFO] [generateCode] HOOK APPLIED: Created ${hookItem.name} from hook.`
236
- );
237
- continue;
238
- }
239
-
240
- const mergedContent = mergeHookIntoSource(
241
- fs.readFileSync(sourceFile, 'utf8'),
242
- fs.readFileSync(hookFile, 'utf8'),
243
- sourceFile
244
- );
245
- fs.writeFileSync(sourceFile, mergedContent);
246
- console.log(
247
- `[INFO] [generateCode] HOOK APPLIED: Merged ${hookItem.name} into source.`
248
- );
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);
249
279
  }
250
- } else if (hasSource) {
251
- fs.mkdirSync(path.join(currentDir, 'hook'), { recursive: true });
252
280
  }
253
281
 
254
- for (const item of items) {
255
- if (!item.isDirectory() || item.name === 'source' || item.name === 'hook')
256
- continue;
257
- processSourceHooks(path.join(currentDir, item.name));
258
- }
259
- }
282
+ console.log(
283
+ '[INFO] [generateCode] STATUS=HOOKS | Processing hook folders beside source folders...'
284
+ );
285
+ processSourceHooks(generatorOptions.outputPath);
260
286
 
261
- // Restore backed-up hooks
262
- if (hookDirs.size > 0 || hookBackups.length > 0) {
263
287
  console.log(
264
- `[INFO] [generateCode] STATUS=RESTORE | Restoring ${hookBackups.length} hook files...`
288
+ '[INFO] [generateCode] STATUS=FINISHED | Code generation complete.'
265
289
  );
266
- for (const relativeHookDir of hookDirs) {
267
- fs.mkdirSync(path.join(outputPath, relativeHookDir), { recursive: true });
268
- }
269
- for (const backup of hookBackups) {
270
- const hookDir = path.join(outputPath, backup.relativePath);
271
- fs.writeFileSync(path.join(hookDir, backup.name), backup.content);
272
- }
273
290
  }
274
-
275
- processSourceHooks(generatorOptions.outputPath);
276
-
277
- console.log(
278
- '[INFO] [generateCode] STATUS=FINISHED | Code generation complete.'
279
- );
280
291
  }
292
+
@@ -105,6 +105,17 @@ async function writeSource({ templatePath, templateData, setting }, { checkCreat
105
105
  return [true, {}];
106
106
  }
107
107
 
108
+ /**
109
+ * Result-Driven generator to generate Integration Tests.
110
+ * Does not have createSource/libs dependencies.
111
+ *
112
+ * @param {string} rootPath - The root path of the project.
113
+ * @param {object} [options={}] - Configuration options.
114
+ * @param {string} [options.outputPath] - Output directory path.
115
+ * @param {string|null} [options.s3Prefix=null] - S3 key prefix for integration test resources.
116
+ * @param {object} [options._izContext] - Integration context containing logger interface.
117
+ * @returns {Promise<{serviceTag: string, count: number, durationMs: number}>}
118
+ */
108
119
  export async function generateIntTest(rootPath, options = {}) {
109
120
  const {
110
121
  outputPath = rootPath,
@@ -45,6 +45,15 @@ function writeSource({ templatePath, templateData, setting }, { checkCreateSourc
45
45
  return [true, {}];
46
46
  }
47
47
 
48
+ /**
49
+ * Result-Driven generator to generate Integration Test resources.
50
+ *
51
+ * @param {string} rootPath - The root path of the project.
52
+ * @param {object} [options={}] - Configuration options.
53
+ * @param {string} [options.outputPath] - Output directory path.
54
+ * @param {object} [options._izContext] - Integration context containing logger interface.
55
+ * @returns {Promise<{serviceTag: string, count: number, durationMs: number}>}
56
+ */
48
57
  export async function generateResourceIntTest(rootPath, options = {}) {
49
58
  const {
50
59
  outputPath = rootPath,
@@ -44,7 +44,7 @@ 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
48
  );
49
49
  const event = await generateEvent(_izContext, allIntTestConfig, savePath, serviceTag);
50
50
  const test = await generateTest(_izContext, allIntTestConfig, savePath, serviceTag);
@@ -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
- intTestPath, // :white_check_mark: ใช้ relative
399
- path.join(rootPath, 'src/'), // :white_check_mark: root ที่ถูก (refactor: ใช้ rootPath แทน process.cwd())
406
+ path.basename(targetLibsPath),
407
+ path.dirname(targetLibsPath),
400
408
  { exceptFileName: 'saveConfig' }
401
409
  );
402
410
  }
@@ -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
- export async function generateSchema(rootPath, options) {
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
- // Parse service config to get serviceTag
24
- const configPath = options?.configPath || path.join(rootPath, 'config', 'serverless.config.yml');
25
- let serviceTag = 'unknownService';
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
- console.log(
61
- '[INFO] [generateSchema] STATUS=FINISHED | Dynamic schemas generated.'
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
  }
@@ -65,8 +65,8 @@ export async function generateRelationshipFlowSchemas(
65
65
  let outputTopic = true;
66
66
  let hasCompleteStep = true;
67
67
 
68
- if (action === 'getRelationship') {
69
- events = "['lambdaSyncInv', 'lambdaSyncApi']";
68
+ if (action === 'GetRelationship') {
69
+ events = "['lambdaSyncInv', 'lambdaSyncApi', 'dsq', 'ownTopic']";
70
70
  outputTopic = false;
71
71
  hasCompleteStep = false;
72
72
  }
@@ -6,8 +6,8 @@ export default [
6
6
  {
7
7
  objectType: '<%= objectType %>',
8
8
  addOnDataStructure: [],
9
- canDelete: true,
10
9
  storageResources: <%- storageResources %>,
10
+ fieldNames: {},
11
11
  extendObjType: <%- extendObjType %>
12
12
  }
13
13
  ];