@izara_project/izara-core-generate-service-code 1.0.67 → 1.0.69

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.
Files changed (128) hide show
  1. package/.prettierrc +11 -0
  2. package/package.json +16 -12
  3. package/scripts/format-ejs.js +104 -0
  4. package/src/compile/compileProject.js +40 -9
  5. package/src/compile/loadSchemas.js +120 -11
  6. package/src/emit/crud/emitCrudApplicationArtifacts.js +188 -41
  7. package/src/emit/crud/emitDynamicCrudFlowSchemas.js +34 -18
  8. package/src/emit/crud/generateCrudApplicationArtifacts.js +14 -6
  9. package/src/emit/crud/generateDynamicCrudFlowSchemas.js +4 -1
  10. package/src/emit/crud/templates/CreateEndpoint_Main.ejs +296 -105
  11. package/src/emit/crud/templates/DeleteEndpoint_Main.ejs +222 -144
  12. package/src/emit/crud/templates/FlowEndpointBeforeLogical_Main.ejs +22 -6
  13. package/src/emit/crud/templates/FlowEndpointComplete_Main.ejs +134 -43
  14. package/src/emit/crud/templates/FlowEndpoint_HdrWbs.ejs +51 -31
  15. package/src/emit/crud/templates/FlowHandler_HdrApi.ejs +53 -45
  16. package/src/emit/crud/templates/FlowHandler_HdrInv.ejs +28 -26
  17. package/src/emit/crud/templates/FlowHandler_HdrSqs.ejs +15 -11
  18. package/src/emit/crud/templates/FlowMain_Wbs.ejs +53 -38
  19. package/src/emit/crud/templates/GetEndpoint_Main.ejs +6 -3
  20. package/src/emit/crud/templates/GetRangeEndpoint_Main.ejs +2 -2
  21. package/src/emit/crud/templates/UpdateEndpoint_Main.ejs +402 -249
  22. package/src/emit/custom/customFlows.js +68 -28
  23. package/src/emit/custom/emitCustomFlowApplicationArtifacts.js +520 -123
  24. package/src/emit/custom/generateCustomFlowApplicationArtifacts.js +28 -8
  25. package/src/emit/custom/templates/FlowEndpoint_Main.ejs +48 -36
  26. package/src/emit/custom/templates/FlowStep_Main.ejs +7 -9
  27. package/src/emit/custom/templates/s3/ConfirmReserved_HdrSqs.ejs +59 -54
  28. package/src/emit/custom/templates/s3/ConfirmReserved_Main.ejs +33 -23
  29. package/src/emit/custom/templates/s3/CreateObject_HdrWbs.ejs +30 -23
  30. package/src/emit/custom/templates/s3/CreateObject_Main.ejs +56 -39
  31. package/src/emit/custom/templates/s3/CreatePreSignUrl_HdrSqs.ejs +56 -51
  32. package/src/emit/custom/templates/s3/CreatePreSignUrl_Main.ejs +112 -75
  33. package/src/emit/custom/templates/s3/FlowHandler_HdrS3.ejs +14 -9
  34. package/src/emit/custom/templates/s3/ProcessS3File_Main.ejs +11 -8
  35. package/src/emit/custom/templates/s3/ReservedLimit_HdrSqs.ejs +65 -56
  36. package/src/emit/custom/templates/s3/ReservedLimit_Main.ejs +74 -52
  37. package/src/emit/custom/templates/triggerCache/CheckTriggerCache_HdrSqs.ejs +58 -14
  38. package/src/emit/custom/templates/triggerCache/CheckTriggerCache_Main.ejs +12 -17
  39. package/src/emit/custom/templates/triggerCache/TriggerCacheComplete_Main.ejs +20 -13
  40. package/src/emit/custom/templates/triggerCache/TriggerCache_Main.ejs +4 -1
  41. package/src/emit/function/functionYamlArtifact.js +272 -79
  42. package/src/emit/function/templates/ApiFunction_Yaml.ejs +7 -0
  43. package/src/emit/function/templates/EventBridgeFunction_Yaml.ejs +6 -0
  44. package/src/emit/function/templates/InvFunction_Yaml.ejs +5 -0
  45. package/src/emit/function/templates/S3Function_Yaml.ejs +16 -0
  46. package/src/emit/function/templates/SqsFunction_Yaml.ejs +10 -0
  47. package/src/emit/function/templates/WbsFunction_Yaml.ejs +10 -0
  48. package/src/emit/rbac/emitDynamicRbacFlowSchemas.js +40 -29
  49. package/src/emit/rbac/emitRbacApplicationArtifacts.js +171 -72
  50. package/src/emit/rbac/generateCreateTargetRoleHandler.js +6 -4
  51. package/src/emit/rbac/generateDynamicRbacFlowSchemas.js +8 -2
  52. package/src/emit/rbac/rbacFlowTag.js +3 -1
  53. package/src/emit/rbac/templates/CreateTargetRole_HdrSqs.ejs +41 -18
  54. package/src/emit/rbac/templates/CreateTargetRole_Main.ejs +23 -3
  55. package/src/emit/rbac/templates/ListRbac_HdrApi.ejs +27 -5
  56. package/src/emit/rbac/templates/ListRbac_HdrSqs.ejs +41 -8
  57. package/src/emit/rbac/templates/ListRbac_Main.ejs +26 -6
  58. package/src/emit/rbac/templates/RbacMutation_Main.ejs +23 -3
  59. package/src/emit/rbac/templates/UserRbacFlow_HdrSqs.ejs +46 -0
  60. package/src/emit/rbac/templates/UserRbacFlow_Main.ejs +152 -33
  61. package/src/emit/rbac/templates/UserRoleMutation_Main.ejs +24 -4
  62. package/src/emit/relationship/emitDynamicRelationshipFlowSchemas.js +26 -11
  63. package/src/emit/relationship/emitRelationshipApplicationArtifacts.js +179 -37
  64. package/src/emit/relationship/generateDynamicRelationshipFlowSchemas.js +8 -2
  65. package/src/emit/relationship/generateRelationshipApplicationArtifacts.js +15 -6
  66. package/src/emit/relationship/templates/ChangeRelationshipComplete_Main.ejs +64 -37
  67. package/src/emit/relationship/templates/ChangeRelationship_Main.ejs +258 -142
  68. package/src/emit/relationship/templates/CreateRelationshipComplete_Main.ejs +68 -43
  69. package/src/emit/relationship/templates/CreateRelationship_Main.ejs +315 -171
  70. package/src/emit/relationship/templates/DeleteRelationshipComplete_Main.ejs +67 -41
  71. package/src/emit/relationship/templates/DeleteRelationship_Main.ejs +226 -115
  72. package/src/emit/relationship/templates/FlowHandler_HdrApi.ejs +41 -33
  73. package/src/emit/relationship/templates/FlowHandler_HdrDsq.ejs +15 -11
  74. package/src/emit/relationship/templates/FlowHandler_HdrInv.ejs +28 -26
  75. package/src/emit/relationship/templates/FlowHandler_HdrSqs.ejs +15 -11
  76. package/src/emit/relationship/templates/GetRelationshipComplete_Main.ejs +46 -26
  77. package/src/emit/relationship/templates/GetRelationship_Main.ejs +233 -126
  78. package/src/emit/relationship/templates/MoveRelationshipComplete_Main.ejs +72 -44
  79. package/src/emit/relationship/templates/MoveRelationship_Main.ejs +266 -150
  80. package/src/emit/relationship/templates/UpdateRelationshipComplete_Main.ejs +74 -45
  81. package/src/emit/relationship/templates/UpdateRelationship_Main.ejs +285 -152
  82. package/src/emit/resource/emitResourceArtifacts.js +274 -105
  83. package/src/emit/resource/generateResourceArtifacts.js +15 -6
  84. package/src/emit/resource/templates/GeneratedLambdaRoleServiceConfig.ejs +139 -0
  85. package/src/emit/resource/templates/S3Bucket_Yaml.ejs +19 -0
  86. package/src/emit/resource/templates/S3Subscription_Yaml.ejs +7 -0
  87. package/src/emit/resource/templates/WebSocketSubscription_Yaml.ejs +7 -0
  88. package/src/emit/system/emitSystemWebSocketArtifacts.js +61 -165
  89. package/src/emit/system/templates/WebSocketComplete_HdrSqs.ejs +32 -0
  90. package/src/emit/system/templates/WebSocketComplete_Main.ejs +46 -0
  91. package/src/emit/system/templates/WebSocketComplete_Yaml.ejs +19 -0
  92. package/src/emit/system/templates/WebSocketConnect.ejs +25 -0
  93. package/src/emit/system/templates/WebSocketConnect_Yaml.ejs +11 -0
  94. package/src/generate.js +141 -34
  95. package/src/generateIntTest.js +76 -28
  96. package/src/generateResourceIntTest.js +35 -12
  97. package/src/generateResources/IntTest/generateResources/generateResources.js +5 -2
  98. package/src/generateResources/IntTest/generateResources/templateResources.ejs +18 -30
  99. package/src/generateResources/IntTest/generateTests/events/events.js +1 -1
  100. package/src/generateResources/IntTest/generateTests/events/templateEvents.ejs +13 -10
  101. package/src/generateResources/IntTest/generateTests/generateTemplate.js +2 -2
  102. package/src/generateResources/IntTest/generateTests/pathIntTest/pathIntTest.js +2 -1
  103. package/src/generateResources/IntTest/generateTests/pathIntTest/template.ejs +1 -3
  104. package/src/generateResources/IntTest/generateTests/tests/templateTests.ejs +1 -1
  105. package/src/generateResources/IntTest/generateTests/tests/tests.js +7 -3
  106. package/src/generateResources/IntTest/libs/{libs.js → IntTestGenerateLib.js} +34 -23
  107. package/src/generateResources/IntTest/upload/uploadIntTest.js +33 -19
  108. package/src/intTestGeneratorShared.js +15 -10
  109. package/src/output/commitArtifacts.js +99 -39
  110. package/src/output/mergeHooks.js +24 -10
  111. package/src/output/validateArtifacts.js +37 -27
  112. package/src/plan/buildGenerationPlan.js +24 -13
  113. package/test/core.test.js +116 -49
  114. package/test/createTargetRoleHandler.test.js +63 -14
  115. package/test/crudApplicationArtifacts.test.js +128 -23
  116. package/test/customFlowApplicationArtifacts.test.js +391 -84
  117. package/test/dynamicCrudFlowSchemas.test.js +74 -26
  118. package/test/dynamicRbacFlowSchemas.test.js +63 -20
  119. package/test/dynamicRelationshipFlowSchemas.test.js +65 -18
  120. package/test/functionYamlArtifact.test.js +67 -10
  121. package/test/intTestApi.test.js +9 -10
  122. package/test/publicApi.test.js +184 -44
  123. package/test/relationshipApplicationArtifacts.test.js +90 -19
  124. package/test/resourceArtifacts.test.js +183 -33
  125. package/docs/test.png +0 -0
  126. package/src/emit/rbac/templates/UserRbacFlow_HdrInv.ejs +0 -10
  127. package/src/emit/relationship/dynamicRelationshipFlowSchemaTemplate.js +0 -11
  128. /package/docs/{test.mmd → TriggeredCacheFlow.mmd} +0 -0
package/.prettierrc ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "printWidth": 80,
3
+ "tabWidth": 2,
4
+ "useTabs": false,
5
+ "semi": true,
6
+ "singleQuote": true,
7
+ "trailingComma": "none",
8
+ "bracketSpacing": true,
9
+ "endOfLine": "lf",
10
+ "plugins": ["prettier-plugin-ejs"]
11
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@izara_project/izara-core-generate-service-code",
3
- "version": "1.0.67",
3
+ "version": "1.0.69",
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": {
@@ -10,31 +10,35 @@
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "author": "Sven Mason <thebarbariansven@gmail.com>",
12
12
  "type": "module",
13
- "main": "index.js",
14
13
  "exports": "./index.js",
14
+ "main": "index.js",
15
15
  "scripts": {
16
- "test": "jest"
16
+ "test": "jest",
17
+ "format:ejs": "node scripts/format-ejs.js"
17
18
  },
18
19
  "jest": {
19
20
  "testEnvironment": "node"
20
21
  },
21
22
  "dependencies": {
23
+ "@izara_project/izara-attribute-tree": "^1.0.15",
22
24
  "@izara_project/izara-core-library-core": "^1.0.32",
23
- "@izara_project/izara-core-library-logger": "^1.0.11",
25
+ "@izara_project/izara-property-nodes": "^1.0.16",
24
26
  "@izara_project/izara-shared-core": "^1.0.13",
25
27
  "ejs": "^6.0.1",
26
28
  "js-beautify": "^2.0.3",
27
- "yaml": "^2.9.0",
28
- "@izara_project/izara-property-nodes": "^1.0.16",
29
- "@izara_project/izara-attribute-tree": "^1.0.15"
29
+ "yaml": "^2.9.0"
30
+ },
31
+ "devDependencies": {
32
+ "@izara_project/izara-core-library-logger": "^1.0.11",
33
+ "@izara_project/izara-core-library-service-schemas": "1.0.158",
34
+ "@izara_project/izara-shared-service-schemas": "^1.0.67",
35
+ "jest": "^30.4.2",
36
+ "prettier": "^3.9.6",
37
+ "prettier-plugin-ejs": "^1.0.3"
30
38
  },
31
39
  "peerDependencies": {
40
+ "@izara_project/izara-core-library-logger": "^1.0.11",
32
41
  "@izara_project/izara-core-library-service-schemas": "^1.0.157",
33
42
  "@izara_project/izara-shared-service-schemas": "^1.0.65"
34
- },
35
- "devDependencies": {
36
- "@izara_project/izara-core-library-service-schemas": "1.0.158",
37
- "@izara_project/izara-shared-service-schemas": "^1.0.66",
38
- "jest": "^30.4.2"
39
43
  }
40
44
  }
@@ -0,0 +1,104 @@
1
+ /* Copyright (C) 2026 Sven Mason <http://izara.io>
2
+ * Prettier formatting script for EJS template files under src/emit
3
+ */
4
+
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+ import prettier from 'prettier';
8
+
9
+ function getAllEjsFiles(dirPath, arrayOfFiles = []) {
10
+ const files = fs.readdirSync(dirPath);
11
+ for (const file of files) {
12
+ const fullPath = path.join(dirPath, file);
13
+ if (fs.statSync(fullPath).isDirectory()) {
14
+ arrayOfFiles = getAllEjsFiles(fullPath, arrayOfFiles);
15
+ } else if (file.endsWith('.ejs')) {
16
+ arrayOfFiles.push(fullPath);
17
+ }
18
+ }
19
+ return arrayOfFiles;
20
+ }
21
+
22
+ export async function formatEjsFiles(targetDir = 'src/emit') {
23
+ const ejsFiles = getAllEjsFiles(targetDir);
24
+ let updatedCount = 0;
25
+
26
+ for (const filePath of ejsFiles) {
27
+ const content = fs.readFileSync(filePath, 'utf8');
28
+ const isYaml = filePath.includes('Yaml.ejs') || filePath.includes('Yaml');
29
+
30
+ const placeholders = [];
31
+ let masked = content;
32
+
33
+ masked = masked.replace(/<%[_-]?\s*([\s\S]*?)\s*[_-]?%>/g, (match) => {
34
+ const id = placeholders.length;
35
+ const isExpr = match.startsWith('<%=') || match.startsWith('<%-');
36
+ const isConditionalComma =
37
+ match.includes('?') && (match.includes("','") || match.includes('","'));
38
+
39
+ let replacement;
40
+ if (isYaml) {
41
+ replacement = isExpr ? `EJS_VAL_${id}` : `# __EJS_TAG_${id}__`;
42
+ } else {
43
+ if (isConditionalComma) {
44
+ replacement = `, /*__EJS_COMMA_${id}__*/`;
45
+ } else if (isExpr) {
46
+ replacement = `__EJS_VAR_${id}__`;
47
+ } else {
48
+ replacement = `/*__EJS_STMT_${id}__*/`;
49
+ }
50
+ }
51
+ placeholders.push({
52
+ id,
53
+ original: match,
54
+ replacement,
55
+ isConditionalComma
56
+ });
57
+ return replacement;
58
+ });
59
+
60
+ const parser = isYaml ? 'yaml' : 'babel';
61
+ let formatted = await prettier.format(masked, {
62
+ parser,
63
+ singleQuote: true,
64
+ semi: true,
65
+ tabWidth: 2,
66
+ useTabs: false,
67
+ trailingComma: 'none',
68
+ printWidth: 80,
69
+ endOfLine: 'lf'
70
+ });
71
+
72
+ for (const item of placeholders) {
73
+ if (item.isConditionalComma) {
74
+ formatted = formatted.replace(item.replacement, item.original);
75
+ formatted = formatted.replace(
76
+ `, /*__EJS_COMMA_${item.id}__*/`,
77
+ item.original
78
+ );
79
+ } else {
80
+ formatted = formatted.replace(item.replacement, item.original);
81
+ }
82
+ }
83
+
84
+ // Ensure unix newline at EOF
85
+ if (!formatted.endsWith('\n')) {
86
+ formatted += '\n';
87
+ }
88
+
89
+ if (formatted !== content) {
90
+ fs.writeFileSync(filePath, formatted, 'utf8');
91
+ updatedCount++;
92
+ }
93
+ }
94
+
95
+ return { total: ejsFiles.length, updated: updatedCount };
96
+ }
97
+
98
+ if (process.argv[1] === new URL(import.meta.url).pathname) {
99
+ formatEjsFiles().then(({ total, updated }) => {
100
+ console.log(
101
+ `Successfully formatted ${updated} / ${total} EJS template files in src/emit.`
102
+ );
103
+ });
104
+ }
@@ -1,26 +1,57 @@
1
+ import Logger from '@izara_project/izara-core-library-logger';
1
2
  import { join, resolve } from 'node:path';
3
+ import { readFileSync, existsSync } from 'node:fs';
4
+ import { parse as parseYaml } from 'yaml';
2
5
 
3
6
  export function compileProject(request = {}) {
4
- if (!isNonEmptyString(request.rootPath)) {
5
- throw new TypeError('GenerationRequest.rootPath must be a non-empty string');
7
+ if (!_isNonEmptyString(request.rootPath)) {
8
+ throw new TypeError(
9
+ 'GenerationRequest.rootPath must be a non-empty string'
10
+ );
6
11
  }
7
12
 
8
13
  const rootPath = resolve(request.rootPath);
9
- const outputPath = resolve(request.outputPath ?? rootPath);
10
- const configPath = resolve(
11
- request.configPath ?? join(rootPath, 'config', 'serverless.config.yml')
12
- );
14
+ let outputPath = rootPath;
15
+ if (request.outputPath) {
16
+ outputPath = resolve(request.outputPath);
17
+ }
18
+ let configPath = join(rootPath, 'config', 'serverless.config.yml');
19
+ if (request.configPath) {
20
+ configPath = resolve(request.configPath);
21
+ }
22
+
23
+ let serviceTag = request.serviceTag;
24
+ if (!serviceTag && existsSync(configPath)) {
25
+ try {
26
+ const configContent = readFileSync(configPath, 'utf8');
27
+ const parsedConfig = parseYaml(configContent);
28
+ if (parsedConfig && parsedConfig.main_config) {
29
+ if (parsedConfig.main_config.iz_serviceTag) {
30
+ serviceTag = parsedConfig.main_config.iz_serviceTag;
31
+ } else if (parsedConfig.main_config.iz_serviceName) {
32
+ serviceTag = parsedConfig.main_config.iz_serviceName;
33
+ }
34
+ }
35
+ } catch (err) {
36
+ Logger?.debug?.('Could not read serverless.config.yml', err);
37
+ }
38
+ }
39
+
40
+ let schemaTemplateConfig = [];
41
+ if (request.schemaTemplateConfig) {
42
+ schemaTemplateConfig = request.schemaTemplateConfig;
43
+ }
13
44
 
14
45
  return Object.freeze({
15
46
  rootPath,
16
47
  outputPath,
17
48
  configPath,
18
49
  schemaRoot: join(rootPath, 'app', 'src', 'schemas'),
19
- serviceTag: request.serviceTag,
20
- schemaTemplateConfig: request.schemaTemplateConfig ?? []
50
+ serviceTag,
51
+ schemaTemplateConfig
21
52
  });
22
53
  }
23
54
 
24
- function isNonEmptyString(value) {
55
+ function _isNonEmptyString(value) {
25
56
  return typeof value === 'string' && value.trim() !== '';
26
57
  }
@@ -1,7 +1,11 @@
1
1
  import { readdir, stat } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
3
  import { pathToFileURL } from 'node:url';
4
- import { utils } from '@izara_project/izara-core-library-service-schemas';
4
+ import {
5
+ getObjectSchema,
6
+ utils
7
+ } from '@izara_project/izara-core-library-service-schemas';
8
+ import Logger from '@izara_project/izara-core-library-logger';
5
9
 
6
10
  export async function loadLocalSchemaDirectory(project, directoryName) {
7
11
  const directoryPath = join(project.schemaRoot, directoryName);
@@ -15,31 +19,61 @@ export async function loadLocalSchemaDirectory(project, directoryName) {
15
19
  throw error;
16
20
  }
17
21
 
22
+ const validEntries = entries.filter(
23
+ (entry) => entry.isFile() && entry.name.endsWith('.js')
24
+ );
25
+ validEntries.sort((a, b) => a.name.localeCompare(b.name));
26
+
18
27
  const schemas = [];
19
- for (const entry of entries.filter(entry => entry.isFile() && entry.name.endsWith('.js')).sort((a, b) => a.name.localeCompare(b.name))) {
28
+ for (const entry of validEntries) {
20
29
  const filePath = join(directoryPath, entry.name);
21
30
  const details = await stat(filePath);
22
- const module = await import(`${pathToFileURL(filePath).href}?schemaVersion=${details.mtimeMs}-${details.size}`);
31
+ const module = await import(
32
+ `${pathToFileURL(filePath).href}?schemaVersion=${details.mtimeMs}-${details.size}`
33
+ );
23
34
  if (!module.default || typeof module.default !== 'object') {
24
- throw new TypeError(`Schema module must default-export an object or array: ${filePath}`);
35
+ throw new TypeError(
36
+ `Schema module must default-export an object or array: ${filePath}`
37
+ );
38
+ }
39
+ let loadedSchemas = [module.default];
40
+ if (Array.isArray(module.default)) {
41
+ loadedSchemas = module.default;
25
42
  }
26
- const loadedSchemas = Array.isArray(module.default) ? module.default : [module.default];
27
43
 
28
- if (process.env.NODE_ENV !== 'test') {
44
+ if (process.env.NODE_ENV && process.env.NODE_ENV !== 'test') {
29
45
  for (const schema of loadedSchemas) {
30
46
  let status = true;
31
47
  let errors = [];
32
-
48
+
33
49
  if (directoryName === 'ObjectSchemas') {
34
- [status, errors] = utils.validateBasicObjectSchema({}, schema);
50
+ [status, errors] = utils.validateObjectSchema(
51
+ { logger: Logger },
52
+ schema
53
+ );
35
54
  } else if (directoryName === 'RelationshipSchemas') {
36
- [status, errors] = utils.validateRelationshipSchema({}, schema);
55
+ if (entry.name.toLowerCase() === 'refobjectrelationships.js') {
56
+ [status, errors] = utils.validateRefRelationshipSchema(
57
+ { logger: Logger },
58
+ schema
59
+ );
60
+ } else {
61
+ [status, errors] = utils.validateRelationshipSchema(
62
+ { logger: Logger },
63
+ schema
64
+ );
65
+ }
37
66
  } else if (directoryName === 'FlowSchemas') {
38
- [status, errors] = utils.validateBasicFlowSchema({}, schema);
67
+ [status, errors] = utils.validateBasicFlowSchema(
68
+ { logger: Logger },
69
+ schema
70
+ );
39
71
  }
40
72
 
41
73
  if (!status) {
42
- throw new Error(`Schema validation failed for ${entry.name}:\n${errors.join('\n')}`);
74
+ throw new Error(
75
+ `Schema validation failed for ${entry.name}:\n${errors.join('\n')}`
76
+ );
43
77
  }
44
78
  }
45
79
  }
@@ -48,3 +82,78 @@ export async function loadLocalSchemaDirectory(project, directoryName) {
48
82
  }
49
83
  return schemas;
50
84
  }
85
+
86
+ /**
87
+ * Resolves extendObjType for ObjectSchemas by fetching the parent schema
88
+ * from S3 and merging its storageResources into the child schema.
89
+ *
90
+ * Only schemas with `extendObjType` are processed; others are returned as-is.
91
+ * If `bucketName` is not provided, falls back to the
92
+ * `iz_serviceSchemaBucketName` environment variable.
93
+ *
94
+ * @param {Object[]} objectSchemas - Array of local ObjectSchemas from
95
+ * loadLocalSchemaDirectory.
96
+ * @param {string} [bucketName] - S3 bucket name for fetching parent schemas.
97
+ * @returns {Promise<Object[]>} The same array with extended storageResources
98
+ * merged in.
99
+ */
100
+ export async function resolveExtendedObjectSchemas(objectSchemas, bucketName) {
101
+ if (!Array.isArray(objectSchemas)) {
102
+ throw new TypeError('objectSchemas must be an array');
103
+ }
104
+ const resolvedSchemas = [];
105
+ for (const schema of objectSchemas) {
106
+ if (!schema.hasOwnProperty('extendObjType')) {
107
+ resolvedSchemas.push(schema);
108
+ continue;
109
+ }
110
+ try {
111
+ // Track which storageResource keys existed in the child before merge
112
+ const initialResources = schema.storageResources || {};
113
+ const childStorageResourceKeys = new Set(Object.keys(initialResources));
114
+ const parentServiceTag = schema.extendObjType.serviceTag;
115
+
116
+ const mergedSchema = await getObjectSchema.mergeExtendObjSchema(
117
+ { logger: Logger },
118
+ schema,
119
+ bucketName
120
+ );
121
+
122
+ // Separate parent DynamoDB resources from child's own storageResources.
123
+ // Parent DynamoDB tables exist in the parent service — they must NOT be
124
+ // re-created here. Instead, track them as external references so that
125
+ // emitResourceArtifacts can add cross-service ARNs to IAM roles.
126
+ const externalDynamoResources = [];
127
+ const ownStorageResources = {};
128
+ const mergedStorageResources = mergedSchema.storageResources || {};
129
+ for (const [key, resource] of Object.entries(mergedStorageResources)) {
130
+ if (
131
+ resource?.storageType === 'dynamoDB' &&
132
+ !childStorageResourceKeys.has(key)
133
+ ) {
134
+ externalDynamoResources.push({
135
+ serviceTag: parentServiceTag,
136
+ tableName: resource.tableName
137
+ });
138
+ } else {
139
+ ownStorageResources[key] = resource;
140
+ }
141
+ }
142
+
143
+ mergedSchema.storageResources = ownStorageResources;
144
+ if (externalDynamoResources.length > 0) {
145
+ mergedSchema.externalDynamoResources = externalDynamoResources;
146
+ }
147
+
148
+ resolvedSchemas.push(mergedSchema);
149
+ } catch (error) {
150
+ Logger.error(
151
+ `resolveExtendedObjectSchemas: failed to merge extendObjType` +
152
+ ` for objectType=${schema.objectType}`,
153
+ { error }
154
+ );
155
+ throw error;
156
+ }
157
+ }
158
+ return resolvedSchemas;
159
+ }