@izara_project/izara-core-generate-service-code 1.0.98 → 1.0.99

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 (34) hide show
  1. package/package.json +1 -1
  2. package/src/compile/flowValidator.js +45 -8
  3. package/src/compile/loadSchemas.js +0 -3
  4. package/src/emit/application/coreFunction/emitCoreFunctionArtifacts.js +15 -1
  5. package/src/emit/application/coreFunction/templates/InitialSetupConfig.ejs +8 -4
  6. package/src/emit/application/crud/templates/CreateEndpoint_Main.ejs +4 -4
  7. package/src/emit/application/crud/templates/CreateLogicalLib.ejs +60 -8
  8. package/src/emit/application/crud/templates/DeleteEndpoint_Main.ejs +4 -4
  9. package/src/emit/application/crud/templates/UpdateEndpoint_Main.ejs +4 -4
  10. package/src/emit/application/custom/customFlowS3.js +4 -4
  11. package/src/emit/application/custom/templates/FlowEndpoint_Main.ejs +2 -2
  12. package/src/emit/application/rbac/emitDynamicRbacFlowSchemas.js +4 -1
  13. package/src/emit/application/rbac/emitRbacApplicationArtifacts.js +76 -1
  14. package/src/emit/application/rbac/templates/ReservedDynamicUsage_Main.ejs +2 -0
  15. package/src/emit/application/rbac/templates/ReservedUsageTtlExpired_HdrDss.ejs +176 -0
  16. package/src/emit/infrastructure/functions/functionYamlArtifact.js +28 -0
  17. package/src/emit/infrastructure/functions/templates/DssFunction_Yaml.ejs +21 -0
  18. package/src/emit/infrastructure/resources/iamResourceArtifacts.js +45 -5
  19. package/src/emit/infrastructure/resources/templates/GeneratedLambdaRoleServiceConfig.ejs +5 -3
  20. package/src/emit/schemas/attributeTree/emitAttributeTreeSchemas.js +62 -1
  21. package/src/generate.js +2 -6
  22. package/src/output/mergeHooks.js +105 -5
  23. package/src/plan/domainRegistry.js +0 -10
  24. package/task.md +40 -96
  25. package/test/integration/graphDrivenFlowSchema.test.js +58 -0
  26. package/test/unit/application/createTargetRoleHandler.test.js +70 -1
  27. package/test/unit/application/crudApplicationArtifacts.test.js +4 -0
  28. package/test/unit/infrastructure/resourceArtifacts.test.js +52 -0
  29. package/test/unit/output/mergeHooks.test.js +137 -0
  30. package/test/unit/schemas/attributeTreeSchemas.test.js +32 -3
  31. package/test/unit/schemas/attributeTreeSingleService.test.js +175 -0
  32. package/test/unit/templates/hookTagsValidation.test.js +83 -0
  33. package/src/emit/schemas/attributeTree/emitAttributeTreeTargetValueStandard.js +0 -132
  34. package/test/unit/schemas/attributeTreeDirectives.test.js +0 -177
@@ -0,0 +1,176 @@
1
+ /*
2
+ Copyright (C) 2020 Sven Mason <http://izara.io>
3
+
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.
8
+
9
+ This program is distributed in the hope that it will be useful,
10
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ GNU Affero General Public License for more details.
13
+
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/>.
16
+ */
17
+
18
+ import { middlewareHandler as middleware } from '@izara_project/izara-middleware';
19
+ import Logger from '@izara_project/izara-core-library-logger';
20
+ import dynamodbSharedLib from '@izara_project/izara-core-library-dynamodb';
21
+
22
+ /**
23
+ * DynamoDB Stream handler for TTL expired records in RunningTotalReservedUsage.
24
+ * Automatically refunds abandoned reservations with atomic decrement.
25
+ */
26
+ export const main = middleware.wrap(async (event, context) => {
27
+ try {
28
+ if (!event || !Array.isArray(event.Records)) {
29
+ Logger.debug('No records in DynamoDB stream event');
30
+ return;
31
+ }
32
+
33
+ let _izContext = event._izContext;
34
+ if (!_izContext) {
35
+ _izContext = {
36
+ logger: Logger,
37
+ correlationIds: new Map()
38
+ };
39
+ }
40
+
41
+ for (const record of event.Records) {
42
+ let recordContext = record._izContext;
43
+ if (!recordContext) {
44
+ recordContext = _izContext;
45
+ }
46
+ let recordLogger = recordContext.logger;
47
+ if (!recordLogger) {
48
+ recordLogger = Logger;
49
+ }
50
+
51
+ const isTtlDelete =
52
+ record.eventName === 'REMOVE' &&
53
+ record.userIdentity &&
54
+ record.userIdentity.type === 'Service' &&
55
+ record.userIdentity.principalId === 'dynamodb.amazonaws.com';
56
+
57
+ if (!isTtlDelete) {
58
+ continue;
59
+ }
60
+
61
+ const oldImage = record.dynamodb && record.dynamodb.OldImage;
62
+ if (!oldImage) {
63
+ continue;
64
+ }
65
+
66
+ let oldStatus = null;
67
+ if (oldImage.runningTotalUsageStatus && oldImage.runningTotalUsageStatus.S) {
68
+ oldStatus = oldImage.runningTotalUsageStatus.S;
69
+ }
70
+
71
+ if (oldStatus !== 'reserved') {
72
+ continue;
73
+ }
74
+
75
+ let identifiersConcat = null;
76
+ if (oldImage.identifiersConcat && oldImage.identifiersConcat.S) {
77
+ identifiersConcat = oldImage.identifiersConcat.S;
78
+ }
79
+ let flowTypeConcat = null;
80
+ if (oldImage.flowTypeConcat && oldImage.flowTypeConcat.S) {
81
+ flowTypeConcat = oldImage.flowTypeConcat.S;
82
+ }
83
+
84
+ if ((!identifiersConcat || !flowTypeConcat) && oldImage.runningTotalReservedId && oldImage.runningTotalReservedId.S) {
85
+ const reservedId = oldImage.runningTotalReservedId.S;
86
+ const lastUnderscoreIndex = reservedId.lastIndexOf('_');
87
+ if (lastUnderscoreIndex > 0) {
88
+ if (!identifiersConcat) {
89
+ identifiersConcat = reservedId.substring(0, lastUnderscoreIndex);
90
+ }
91
+ if (!flowTypeConcat) {
92
+ flowTypeConcat = reservedId.substring(lastUnderscoreIndex + 1);
93
+ }
94
+ }
95
+ }
96
+
97
+ let usageCount = 1;
98
+ if (oldImage.usageCount && oldImage.usageCount.N) {
99
+ usageCount = Number(oldImage.usageCount.N);
100
+ }
101
+
102
+ if (identifiersConcat && flowTypeConcat && usageCount > 0) {
103
+ const usageTable = dynamodbSharedLib.tableName(
104
+ recordContext,
105
+ 'RunningTotalUsage'
106
+ );
107
+
108
+ const usageRecord = await dynamodbSharedLib.getItem(
109
+ recordContext,
110
+ usageTable,
111
+ {
112
+ identifiersConcat: identifiersConcat,
113
+ flowTypeConcat: flowTypeConcat
114
+ }
115
+ );
116
+
117
+ if (usageRecord) {
118
+ let currentTotal = 0;
119
+ if (typeof usageRecord.totalCount === 'number') {
120
+ currentTotal = usageRecord.totalCount;
121
+ }
122
+ const newTotal = Math.max(0, currentTotal - usageCount);
123
+ const updateUuid = recordContext.uniqueRequestId || `${Date.now()}`;
124
+
125
+ const queryElementsWhenUpdate = {
126
+ additionalAttributes: { currentUuid: usageRecord.updateUuid },
127
+ logicalElements: [
128
+ {
129
+ type: 'logical',
130
+ comparison: 'equals',
131
+ lhsAttribute: 'updateUuid',
132
+ rhsReference: 'currentUuid'
133
+ }
134
+ ],
135
+ returnValues: 'ALL_NEW'
136
+ };
137
+
138
+ const updateAttributes = [
139
+ { attributeName: 'totalCount', action: 'set', value: newTotal },
140
+ { attributeName: 'updateUuid', action: 'set', value: updateUuid }
141
+ ];
142
+
143
+ await dynamodbSharedLib.updateItem(
144
+ recordContext,
145
+ usageTable,
146
+ {
147
+ identifiersConcat: identifiersConcat,
148
+ flowTypeConcat: flowTypeConcat
149
+ },
150
+ updateAttributes,
151
+ queryElementsWhenUpdate,
152
+ {
153
+ complexAttributes: true,
154
+ errorOnConditionalExpNotPass: false
155
+ }
156
+ ).catch(() => null);
157
+
158
+ recordLogger.info({
159
+ message: '[ReservedUsageTtlExpired] Auto-refunded expired reservation',
160
+ identifiersConcat: identifiersConcat,
161
+ flowTypeConcat: flowTypeConcat,
162
+ usageCount: usageCount,
163
+ previousTotal: currentTotal,
164
+ newTotal: newTotal
165
+ });
166
+ }
167
+ }
168
+ }
169
+ } catch (err) {
170
+ Logger.error({
171
+ message: 'Unhandled error in ReservedUsageTtlExpired_HdrDss',
172
+ error: err
173
+ });
174
+ throw err;
175
+ }
176
+ });
@@ -198,6 +198,34 @@ export function renderS3FunctionYaml({
198
198
  });
199
199
  }
200
200
 
201
+ export function renderDssFunctionYaml({
202
+ functionName,
203
+ handlerPath,
204
+ roleName,
205
+ tableName,
206
+ batchSize = 100,
207
+ startingPosition = 'TRIM_HORIZON',
208
+ filterPatterns = [],
209
+ flowTag,
210
+ flowStepName
211
+ }) {
212
+ return _renderFunctionTemplate('DssFunction_Yaml.ejs', {
213
+ functionName,
214
+ handlerPath,
215
+ roleName,
216
+ tableName,
217
+ batchSize,
218
+ startingPosition,
219
+ filterPatterns,
220
+ environmentSetting: _environmentSetting(
221
+ functionName,
222
+ flowTag,
223
+ flowStepName
224
+ ),
225
+ functionSetting: _functionSetting(functionName)
226
+ });
227
+ }
228
+
201
229
  export function renderEventBridgeFunctionYaml({
202
230
  functionName,
203
231
  handlerPath,
@@ -0,0 +1,21 @@
1
+ <%= functionName %>:
2
+ handler: <%= handlerPath %>
3
+ name: ${self:custom.iz_resourcePrefix}<%= functionName %>
4
+ role: <%= roleName %>
5
+ <%- environmentSetting %><%- functionSetting %>
6
+ events:
7
+ - stream:
8
+ type: dynamodb
9
+ arn:
10
+ Fn::GetAtt: [<%= tableName %>Table, StreamArn]
11
+ batchSize: <%= batchSize %>
12
+ startingPosition: <%= startingPosition %>
13
+ <% if (Array.isArray(filterPatterns) && filterPatterns.length > 0) { -%>
14
+ filterPatterns:
15
+ <% filterPatterns.forEach(function(pattern) { -%>
16
+ - eventName: <%- JSON.stringify(pattern.eventName) %>
17
+ userIdentity:
18
+ type: <%- JSON.stringify(pattern.userIdentity.type) %>
19
+ principalId: <%- JSON.stringify(pattern.userIdentity.principalId) %>
20
+ <% }); -%>
21
+ <% } -%>
@@ -96,9 +96,26 @@ export function getRbacValidatorServiceTags(state) {
96
96
  export async function renderLambdaRoleConfig(state) {
97
97
  const s3Flows = state.flows
98
98
  .filter((flow) => includesEvent(flow, 's3'))
99
- .map((flow) => ({
100
- roleName: upperFirst(flow.flowTag)
101
- }));
99
+ .map((flow) => {
100
+ let quotaServiceTag = flow.quotaServiceTag;
101
+ if (!quotaServiceTag && flow.belongTo) {
102
+ if (Array.isArray(flow.belongTo)) {
103
+ quotaServiceTag = flow.belongTo[0]?.serviceTag;
104
+ } else if (flow.belongTo.serviceTag) {
105
+ quotaServiceTag = flow.belongTo.serviceTag;
106
+ }
107
+ }
108
+ if (!quotaServiceTag && flow.objType?.serviceTag) {
109
+ quotaServiceTag = flow.objType.serviceTag;
110
+ }
111
+ if (!quotaServiceTag) {
112
+ quotaServiceTag = state.project?.serviceTag || 'Rbac';
113
+ }
114
+ return {
115
+ roleName: upperFirst(flow.flowTag),
116
+ quotaServiceTag
117
+ };
118
+ });
102
119
  const rbacValidatorServiceTags = getRbacValidatorServiceTags(state);
103
120
  return renderResourceTemplate('GeneratedLambdaRoleServiceConfig.ejs', {
104
121
  crud: state.crud,
@@ -160,6 +177,19 @@ export function roleStatements(state, roleName) {
160
177
  ]
161
178
  });
162
179
  }
180
+ if (roleName === 'FlowRbacRole' && state.selfRbac) {
181
+ statements.push({
182
+ action: [
183
+ 'dynamodb:DescribeStream',
184
+ 'dynamodb:GetRecords',
185
+ 'dynamodb:GetShardIterator',
186
+ 'dynamodb:ListStreams'
187
+ ],
188
+ resource: [
189
+ 'arn:aws:dynamodb:${self:custom.iz_region}:${self:custom.iz_accountId}:table/${self:custom.iz_resourcePrefix}RunningTotalReservedUsage/stream/*'
190
+ ]
191
+ });
192
+ }
163
193
  return statements;
164
194
  }
165
195
 
@@ -508,11 +538,16 @@ export function roleExternalTopicArns(state, roleName) {
508
538
  `arn:aws:sns:\${self:custom.iz_region}:\${self:custom.iz_accountId}:${tag}_\${self:custom.iz_stage}_ValidatorRbac_In`
509
539
  );
510
540
  if (roleName === 'FlowObjectsRole') {
541
+ const extraCrudTopics = extraTags.flatMap((tag) => [
542
+ `arn:aws:sns:\${self:custom.iz_region}:\${self:custom.iz_accountId}:${tag}_\${self:custom.iz_stage}_ValidatorRbac_In`,
543
+ `arn:aws:sns:\${self:custom.iz_region}:\${self:custom.iz_accountId}:${tag}_\${self:custom.iz_stage}_ConfirmUsage_In`,
544
+ `arn:aws:sns:\${self:custom.iz_region}:\${self:custom.iz_accountId}:${tag}_\${self:custom.iz_stage}_CancelUsage_In`
545
+ ]);
511
546
  return [
512
547
  `arn:aws:sns:\${self:custom.iz_region}:\${self:custom.iz_accountId}:Rbac_\${self:custom.iz_stage}_ValidatorRbac_In`,
513
548
  `arn:aws:sns:\${self:custom.iz_region}:\${self:custom.iz_accountId}:Rbac_\${self:custom.iz_stage}_ConfirmUsage_In`,
514
549
  `arn:aws:sns:\${self:custom.iz_region}:\${self:custom.iz_accountId}:Rbac_\${self:custom.iz_stage}_CancelUsage_In`,
515
- ...extraTopics
550
+ ...extraCrudTopics
516
551
  ];
517
552
  }
518
553
  if (roleName === 'FlowRbacRole' || roleName === 'FlowRelationshipRole') {
@@ -536,11 +571,16 @@ export function roleExternalLambdaArns(state, roleName) {
536
571
  `arn:aws:lambda:\${self:custom.iz_region}:\${self:custom.iz_accountId}:function:${tag}\${self:custom.iz_stage}ValidatorRbacHdrInv`
537
572
  );
538
573
  if (roleName === 'FlowObjectsRole') {
574
+ const extraCrudLambdas = extraTags.flatMap((tag) => [
575
+ `arn:aws:lambda:\${self:custom.iz_region}:\${self:custom.iz_accountId}:function:${tag}\${self:custom.iz_stage}ValidatorRbacHdrInv`,
576
+ `arn:aws:lambda:\${self:custom.iz_region}:\${self:custom.iz_accountId}:function:${tag}\${self:custom.iz_stage}ConfirmUsageHdrInv`,
577
+ `arn:aws:lambda:\${self:custom.iz_region}:\${self:custom.iz_accountId}:function:${tag}\${self:custom.iz_stage}CancelUsageHdrInv`
578
+ ]);
539
579
  return [
540
580
  `arn:aws:lambda:\${self:custom.iz_region}:\${self:custom.iz_accountId}:function:Rbac\${self:custom.iz_stage}ValidatorRbacHdrInv`,
541
581
  `arn:aws:lambda:\${self:custom.iz_region}:\${self:custom.iz_accountId}:function:Rbac\${self:custom.iz_stage}ConfirmUsageHdrInv`,
542
582
  `arn:aws:lambda:\${self:custom.iz_region}:\${self:custom.iz_accountId}:function:Rbac\${self:custom.iz_stage}CancelUsageHdrInv`,
543
- ...extraLambdas
583
+ ...extraCrudLambdas
544
584
  ];
545
585
  }
546
586
  if (roleName === 'FlowRbacRole' || roleName === 'FlowRelationshipRole') {
@@ -60,7 +60,9 @@ export function generatedLambdaRole() {
60
60
  `arn:aws:sns:${process.env.iz_region}:${process.env.iz_accountId}:${snsSharedLib.snsTopicNameByFlowSchema(_izContext, 'ConfirmUsage_In', 'Rbac')}`,
61
61
  `arn:aws:sns:${process.env.iz_region}:${process.env.iz_accountId}:${snsSharedLib.snsTopicNameByFlowSchema(_izContext, 'CancelUsage_In', 'Rbac')}`<% if (rbac) { %>,
62
62
  `arn:aws:sns:${process.env.iz_region}:${process.env.iz_accountId}:${snsSharedLib.snsTopicNameByFlowSchema(_izContext, 'ValidatorRbac_In')}`<% } %><% (rbacValidatorServiceTags || []).forEach(function(tag) { %>,
63
- `arn:aws:sns:${process.env.iz_region}:${process.env.iz_accountId}:${snsSharedLib.snsTopicNameByFlowSchema(_izContext, 'ValidatorRbac_In', '<%= tag %>')}`<% }); %>
63
+ `arn:aws:sns:${process.env.iz_region}:${process.env.iz_accountId}:${snsSharedLib.snsTopicNameByFlowSchema(_izContext, 'ValidatorRbac_In', '<%= tag %>')}`,
64
+ `arn:aws:sns:${process.env.iz_region}:${process.env.iz_accountId}:${snsSharedLib.snsTopicNameByFlowSchema(_izContext, 'ConfirmUsage_In', '<%= tag %>')}`,
65
+ `arn:aws:sns:${process.env.iz_region}:${process.env.iz_accountId}:${snsSharedLib.snsTopicNameByFlowSchema(_izContext, 'CancelUsage_In', '<%= tag %>')}`<% }); %>
64
66
  ]
65
67
  }<% } %>
66
68
  //(<createFlowObjectsRole>)
@@ -144,7 +146,7 @@ export function generatedLambdaRole() {
144
146
  Effect: 'Allow',
145
147
  Action: ['sns:Publish'],
146
148
  Resource: [
147
- `arn:aws:sns:${process.env.iz_region}:${process.env.iz_accountId}:${snsSharedLib.snsTopicNameByFlowSchema(_izContext, 'ReservedDynamicUsage_In', 'AccountLimits')}`
149
+ `arn:aws:sns:${process.env.iz_region}:${process.env.iz_accountId}:${snsSharedLib.snsTopicNameByFlowSchema(_izContext, 'ReservedDynamicUsage_In', '<%= flow.quotaServiceTag || 'Rbac' %>')}`
148
150
  ]
149
151
  },
150
152
 
@@ -153,7 +155,7 @@ export function generatedLambdaRole() {
153
155
  Effect: 'Allow',
154
156
  Action: ['sns:Publish'],
155
157
  Resource: [
156
- `arn:aws:sns:${process.env.iz_region}:${process.env.iz_accountId}:${snsSharedLib.snsTopicNameByFlowSchema(_izContext, 'ConfirmDynamicAfterUpload_In', 'AccountLimits')}`
158
+ `arn:aws:sns:${process.env.iz_region}:${process.env.iz_accountId}:${snsSharedLib.snsTopicNameByFlowSchema(_izContext, 'ConfirmDynamicAfterUpload_In', '<%= flow.quotaServiceTag || 'Rbac' %>')}`
157
159
  ]
158
160
  }
159
161
  //(<create<%= flow.roleName %>Role>)
@@ -62,6 +62,60 @@ function _renderRelationshipSchema(relationshipTag, content) {
62
62
  return `export default ${JSON.stringify(wrapper, null, 2)};\n`;
63
63
  }
64
64
 
65
+ function _renderValueStandardSchema(tag, storageResources) {
66
+ const storageResourceTags = Object.keys(storageResources);
67
+ if (storageResourceTags.length === 0) {
68
+ storageResourceTags.push('myGraph');
69
+ }
70
+
71
+ const storageJson = JSON.stringify(storageResources, null, 2)
72
+ .replace(/\n/g, '\n ');
73
+ const tagsJson = JSON.stringify(storageResourceTags);
74
+
75
+ return `export default [
76
+ {
77
+ objectType: '${tag}ValueStandard',
78
+ canDelete: true,
79
+ generatedBy: 'user',
80
+ storageResources: ${storageJson},
81
+ addOnDataStructure: [
82
+ //(<addOnDataStructure${tag}ValueStandard>)
83
+ // {
84
+ // type: 'propertyValue',
85
+ // propertyValuePrefix: '${tag}ValueStandard'
86
+ // }
87
+ //(</addOnDataStructure${tag}ValueStandard>)
88
+ ],
89
+ fieldNames: {
90
+ ${tag}ValueStandardId: {
91
+ type: 'string',
92
+ randomOnCreate: true,
93
+ canUpdate: false,
94
+ storageResourceTags: ${tagsJson}
95
+ },
96
+ label: {
97
+ type: 'string',
98
+ canUpdate: true,
99
+ storageResourceTags: ${tagsJson}
100
+ },
101
+ value: {
102
+ type: 'string',
103
+ requiredOnCreate: true,
104
+ canUpdate: true,
105
+ storageResourceTags: ${tagsJson}
106
+ }
107
+ },
108
+ identifiers: [
109
+ {
110
+ type: 'identifier',
111
+ fieldName: '${tag}ValueStandardId'
112
+ }
113
+ ]
114
+ }
115
+ ];
116
+ `;
117
+ }
118
+
65
119
  export function attributeTreeObjectSchemaManagedPaths(objects = []) {
66
120
  const trees = _extractAttributeTrees(objects);
67
121
  const paths = [];
@@ -71,7 +125,8 @@ export function attributeTreeObjectSchemaManagedPaths(objects = []) {
71
125
  paths.push(
72
126
  `app/src/schemas/ObjectSchemas/generatedCode/attributeTree/${tag}Link.js`,
73
127
  `app/src/schemas/ObjectSchemas/generatedCode/attributeTree/${tag}Value.js`,
74
- `app/src/schemas/ObjectSchemas/generatedCode/attributeTree/${tag}Label.js`
128
+ `app/src/schemas/ObjectSchemas/generatedCode/attributeTree/${tag}Label.js`,
129
+ `app/src/schemas/ObjectSchemas/generatedCode/attributeTree/${tag}ValueStandard.js`
75
130
  );
76
131
  }
77
132
 
@@ -205,6 +260,12 @@ export function emitAttributeTreeObjectSchemas({ objects = [] } = {}) {
205
260
  path: `app/src/schemas/ObjectSchemas/generatedCode/attributeTree/${tag}Label.js`,
206
261
  content: _renderObjectSchema(labelSchema)
207
262
  });
263
+
264
+ // 4. {tag}ValueStandard.js
265
+ artifacts.push({
266
+ path: `app/src/schemas/ObjectSchemas/generatedCode/attributeTree/${tag}ValueStandard.js`,
267
+ content: _renderValueStandardSchema(tag, graphStorageResources)
268
+ });
208
269
  }
209
270
 
210
271
  return artifacts;
package/src/generate.js CHANGED
@@ -4,7 +4,6 @@ import { commitArtifacts } from './output/commitArtifacts.js';
4
4
  import { compileProject } from './compile/compileProject.js';
5
5
  import {
6
6
  loadLocalSchemaDirectory,
7
- loadGenerateDirectives,
8
7
  resolveExtendedObjectSchemas,
9
8
  resolveGraphServiceTags
10
9
  } from './compile/loadSchemas.js';
@@ -22,8 +21,7 @@ export async function generate(rootPath, options = {}) {
22
21
  relationships,
23
22
  flows,
24
23
  notifications,
25
- errors,
26
- directives
24
+ errors
27
25
  ] = await Promise.all([
28
26
  loadLocalSchemaDirectory(project, 'ObjectSchemas', schemaLoadOptions),
29
27
  loadLocalSchemaDirectory(
@@ -37,8 +35,7 @@ export async function generate(rootPath, options = {}) {
37
35
  'NotificationSchemas',
38
36
  schemaLoadOptions
39
37
  ),
40
- loadLocalSchemaDirectory(project, 'ErrorSchemas', schemaLoadOptions),
41
- loadGenerateDirectives(project, schemaLoadOptions)
38
+ loadLocalSchemaDirectory(project, 'ErrorSchemas', schemaLoadOptions)
42
39
  ]);
43
40
  const schemaBucketName =
44
41
  options.schemaBucketName || process.env.iz_serviceSchemaBucketName;
@@ -82,7 +79,6 @@ export async function generate(rootPath, options = {}) {
82
79
  flows,
83
80
  notifications,
84
81
  errors,
85
- directives,
86
82
  objectGraphServiceTags,
87
83
  relationshipGraphServiceTags,
88
84
  logger: options.logger
@@ -40,6 +40,14 @@ async function mergeHook(outputRoot, artifact) {
40
40
  return artifact.content;
41
41
  }
42
42
 
43
+ _validateHookIntegrity(artifact.path, sourceContent);
44
+ _warnOrphanedHooks(
45
+ artifact.path,
46
+ sourceContent,
47
+ artifact.content,
48
+ matchers
49
+ );
50
+
43
51
  const tagOccurrences = new Map();
44
52
  return artifact.content.replace(
45
53
  matchers.anyTag,
@@ -60,13 +68,105 @@ async function mergeHook(outputRoot, artifact) {
60
68
  }
61
69
  const hookBlock = matches[count];
62
70
  if (hookBlock !== null && hookBlock !== undefined) {
63
- return hookBlock;
71
+ return _normalizeHookBlock(hookBlock, tagName);
64
72
  }
65
73
  return _match;
66
74
  }
67
75
  );
68
76
  }
69
77
 
78
+ function _validateHookIntegrity(filePath, content) {
79
+ const lower = filePath.toLowerCase();
80
+ let openCount = 0;
81
+ let closeCount = 0;
82
+
83
+ if (
84
+ lower.endsWith('.js') ||
85
+ lower.endsWith('.mjs') ||
86
+ lower.endsWith('.cjs')
87
+ ) {
88
+ const openP1 = content.match(/\/\/\s*\(<\s*[\w.-]+/g);
89
+ const closeP1 = content.match(/\/\/\s*\(<\/\s*[\w.-]+/g);
90
+ const openP2 = content.match(/\/\/\s*<#\s*[\w.-]+/g);
91
+ const closeP2 = content.match(/\/\/\s*<#\/\s*[\w.-]+/g);
92
+
93
+ if (openP1) {
94
+ openCount += openP1.length;
95
+ }
96
+ if (closeP1) {
97
+ closeCount += closeP1.length;
98
+ }
99
+ if (openP2) {
100
+ openCount += openP2.length;
101
+ }
102
+ if (closeP2) {
103
+ closeCount += closeP2.length;
104
+ }
105
+ } else if (lower.endsWith('.yml') || lower.endsWith('.yaml')) {
106
+ const openYml = content.match(/#\s*<#\s*[\w.-]+/g);
107
+ const closeYml = content.match(/#\s*<#\/\s*[\w.-]+/g);
108
+
109
+ if (openYml) {
110
+ openCount += openYml.length;
111
+ }
112
+ if (closeYml) {
113
+ closeCount += closeYml.length;
114
+ }
115
+ }
116
+
117
+ if (openCount !== closeCount) {
118
+ throw new Error(
119
+ `[mergeHooks] Malformed hook tags in '${filePath}': found ` +
120
+ `${openCount} open tag(s) but ${closeCount} close tag(s). ` +
121
+ `Aborting to prevent code loss.`
122
+ );
123
+ }
124
+ }
125
+
126
+ function _warnOrphanedHooks(
127
+ filePath,
128
+ sourceContent,
129
+ artifactContent,
130
+ matchers
131
+ ) {
132
+ const anyTagRegex = new RegExp(matchers.anyTag.source, 'gm');
133
+ let match = anyTagRegex.exec(sourceContent);
134
+
135
+ while (match !== null) {
136
+ const tagName = _extractTagName(match[1], match[3]);
137
+ let body = '';
138
+ if (typeof match[2] === 'string') {
139
+ body = match[2];
140
+ } else if (typeof match[4] === 'string') {
141
+ body = match[4];
142
+ }
143
+
144
+ if (tagName && body.trim().length > 0) {
145
+ const templatePattern = matchers.tagPattern(tagName);
146
+ const existsInTemplate = templatePattern.test(artifactContent);
147
+ if (!existsInTemplate) {
148
+ console.warn(
149
+ `[WARN] [mergeHooks] Hook '${tagName}' in '${filePath}' ` +
150
+ `contains custom code but is no longer present in template.`
151
+ );
152
+ }
153
+ }
154
+ match = anyTagRegex.exec(sourceContent);
155
+ }
156
+ }
157
+
158
+ function _normalizeHookBlock(hookBlock, tagName) {
159
+ if (typeof hookBlock !== 'string' || hookBlock.length === 0) {
160
+ return hookBlock;
161
+ }
162
+ const esc = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
163
+ const openRegex = new RegExp(`(//\\s*\\(<\\s*${esc}\\s*)(?!>)\\)`);
164
+ const closeRegex = new RegExp(`(//\\s*\\(<\\/\\s*${esc}\\s*)(?!>)\\)`);
165
+ let normalized = hookBlock.replace(openRegex, '$1>)');
166
+ normalized = normalized.replace(closeRegex, '$1>)');
167
+ return normalized;
168
+ }
169
+
70
170
  function _extractTagName(p1, p2) {
71
171
  if (typeof p1 === 'string' && p1.length > 0) {
72
172
  return p1;
@@ -85,8 +185,8 @@ function hookMatchers(filePath) {
85
185
  lower.endsWith('.cjs')
86
186
  ) {
87
187
  const jsP1 =
88
- '\\/\\/\\s*\\(<\\s*([\\w.-]+)\\s*>\\)([\\s\\S]*?)' +
89
- '\\/\\/\\s*\\(<\\/\\s*\\1\\s*>\\)';
188
+ '\\/\\/\\s*\\(<\\s*([\\w.-]+)\\s*>?\\s*\\)([\\s\\S]*?)' +
189
+ '\\/\\/\\s*\\(<\\/\\s*\\1\\s*>?\\s*\\)';
90
190
  const jsP2 =
91
191
  '\\/\\/\\s*<#\\s*([\\w.-]+)\\s*#>([\\s\\S]*?)' +
92
192
  '\\/\\/\\s*<#\\/\\s*\\3\\s*#>';
@@ -95,8 +195,8 @@ function hookMatchers(filePath) {
95
195
  tagPattern: (name) => {
96
196
  const esc = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
97
197
  const p1 =
98
- `\\/\\/\\s*\\(<\\s*${esc}\\s*>\\)([\\s\\S]*?)` +
99
- `\\/\\/\\s*\\(<\\/\\s*${esc}\\s*>\\)`;
198
+ `\\/\\/\\s*\\(<\\s*${esc}\\s*>?\\s*\\)([\\s\\S]*?)` +
199
+ `\\/\\/\\s*\\(<\\/\\s*${esc}\\s*>?\\s*\\)`;
100
200
  const p2 =
101
201
  `\\/\\/\\s*<#\\s*${esc}\\s*#>([\\s\\S]*?)` +
102
202
  `\\/\\/\\s*<#\\/\\s*${esc}\\s*#>`;
@@ -52,10 +52,6 @@ import {
52
52
  attributeTreeObjectSchemaManagedPaths,
53
53
  attributeTreeRelationshipSchemaManagedPaths
54
54
  } from '../emit/schemas/attributeTree/emitAttributeTreeSchemas.js';
55
- import {
56
- emitAttributeTreeTargetValueStandard,
57
- attributeTreeTargetValueStandardManagedPaths
58
- } from '../emit/schemas/attributeTree/emitAttributeTreeTargetValueStandard.js';
59
55
  import {
60
56
  emitLedgerObjectSchemas,
61
57
  emitLedgerRelationshipSchemas,
@@ -81,7 +77,6 @@ export async function resolveEmissionGroups(context) {
81
77
  flows,
82
78
  notifications,
83
79
  errors,
84
- directives = [],
85
80
  objectGraphServiceTags,
86
81
  relationshipGraphServiceTags,
87
82
  logger
@@ -206,11 +201,6 @@ export async function resolveEmissionGroups(context) {
206
201
  managedPaths: rbacFlowSchemaManagedPaths(objects),
207
202
  artifacts: rbacFlowArtifacts
208
203
  },
209
- {
210
- scope: 'attribute-tree-target-value-standard',
211
- managedPaths: attributeTreeTargetValueStandardManagedPaths(directives),
212
- artifacts: emitAttributeTreeTargetValueStandard({ directives })
213
- },
214
204
  {
215
205
  scope: 'attribute-tree-object-schemas',
216
206
  managedPaths: attributeTreeObjectSchemaManagedPaths(objects),