@izara_project/izara-core-generate-service-code 1.0.100 → 1.0.102

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 (26) hide show
  1. package/package.json +1 -1
  2. package/src/emit/application/crud/templates/CreateLogicalLib.ejs +6 -0
  3. package/src/emit/application/custom/customFlowS3.js +226 -6
  4. package/src/emit/application/custom/customFlows.js +22 -14
  5. package/src/emit/application/custom/emitCustomFlowApplicationArtifacts.js +5 -4
  6. package/src/emit/application/custom/templates/s3/ConfirmComplete_HdrSqs.ejs +1 -0
  7. package/src/emit/application/custom/templates/s3/ConfirmComplete_Main.ejs +28 -17
  8. package/src/emit/application/custom/templates/s3/ProcessS3File_Main.ejs +234 -23
  9. package/src/emit/application/custom/templates/s3/Start_HdrWbs.ejs +65 -0
  10. package/src/emit/application/custom/templates/s3/Start_Main.ejs +40 -17
  11. package/src/emit/application/custom/templates/s3/{ConfirmAfterUpload_HdrSqs.ejs → legacy/ConfirmAfterUpload_HdrSqs.ejs} +1 -0
  12. package/src/emit/application/custom/templates/s3/{ConfirmAfterUpload_Main.ejs → legacy/ConfirmAfterUpload_Main.ejs} +1 -1
  13. package/src/emit/infrastructure/functions/templates/DssFunction_Yaml.ejs +1 -1
  14. package/src/emit/infrastructure/resources/dynamoResourceArtifacts.js +6 -2
  15. package/src/plan/domainRegistry.js +2 -1
  16. package/test/unit/application/createTargetRoleHandler.test.js +1 -1
  17. package/test/unit/application/customFlowS3.test.js +284 -2
  18. package/test/unit/infrastructure/resourceArtifacts.test.js +4 -0
  19. /package/src/emit/application/custom/templates/s3/{ConfirmReserved_HdrSqs.ejs → legacy/ConfirmReserved_HdrSqs.ejs} +0 -0
  20. /package/src/emit/application/custom/templates/s3/{ConfirmReserved_Main.ejs → legacy/ConfirmReserved_Main.ejs} +0 -0
  21. /package/src/emit/application/custom/templates/s3/{CreateObject_HdrWbs.ejs → legacy/CreateObject_HdrWbs.ejs} +0 -0
  22. /package/src/emit/application/custom/templates/s3/{CreateObject_Main.ejs → legacy/CreateObject_Main.ejs} +0 -0
  23. /package/src/emit/application/custom/templates/s3/{CreatePreSignUrl_HdrSqs.ejs → legacy/CreatePreSignUrl_HdrSqs.ejs} +0 -0
  24. /package/src/emit/application/custom/templates/s3/{CreatePreSignUrl_Main.ejs → legacy/CreatePreSignUrl_Main.ejs} +0 -0
  25. /package/src/emit/application/custom/templates/s3/{ReservedLimit_HdrSqs.ejs → legacy/ReservedLimit_HdrSqs.ejs} +0 -0
  26. /package/src/emit/application/custom/templates/s3/{ReservedLimit_Main.ejs → legacy/ReservedLimit_Main.ejs} +0 -0
@@ -131,8 +131,8 @@ test('emitS3Flow with ADR-016 deferred-node pipeline generates correct 3 pairs o
131
131
  'Start_Main should generate S3 presigned post URL'
132
132
  );
133
133
  assert.ok(
134
- startMainArtifact.content.includes('uuidv7'),
135
- 'Start_Main should generate identifier with uuidv7'
134
+ startMainArtifact.content.includes('identifierUuid'),
135
+ 'Start_Main should generate identifier with identifierUuid'
136
136
  );
137
137
  assert.ok(
138
138
  !startMainArtifact.content.includes("snsTopicArnByFlowSchema(_izContext, 'Create_In')"),
@@ -186,6 +186,10 @@ test('emitS3Flow with ADR-016 deferred-node pipeline generates correct 3 pairs o
186
186
  procMainArtifact.content.includes('FeedChunkRecordUrlConfirmAfterUploadHdrSqs'),
187
187
  'ProcessAfterUpload_Main must target ConfirmAfterUploadHdrSqs for callback'
188
188
  );
189
+ assert.ok(
190
+ procMainArtifact.content.includes('feedChunkId: rawId'),
191
+ 'ProcessAfterUpload_Main must include objectTypeId: rawId in fieldNames'
192
+ );
189
193
 
190
194
  const confirmMainArtifact = result.artifacts.find(
191
195
  (a) => a.path === `${source}/FeedChunkRecordUrlConfirmAfterUpload_Main.js`
@@ -725,6 +729,284 @@ test('emitS3Flow with ADR-016 pipeline without step-level event enum resolves co
725
729
  ]);
726
730
  });
727
731
 
732
+ test('emitS3Flow generates relationships with direction to in ProcessAfterUpload_Main and fields in Start_Main', async () => {
733
+ const flowWithRel = {
734
+ flowTag: 'FeedChunkRecordUrl',
735
+ initiateBy: 'user',
736
+ handleObj: 'one',
737
+ statusType: 'statusField',
738
+ event: ['s3'],
739
+ objType: {
740
+ objectType: 'feedChunk',
741
+ serviceTag: 'FeedChunk'
742
+ },
743
+ bucketName: 'video-record-temp',
744
+ flowSteps: [
745
+ { stepName: 'Start', event: ['wbs'] },
746
+ { stepName: 'ProcessAfterUpload', event: ['s3'] },
747
+ { stepName: 'ConfirmComplete', event: ['extTopic'] }
748
+ ],
749
+ relationships: [
750
+ {
751
+ targetObjType: { objectType: 'videoFeed', serviceTag: 'VideoFeed' },
752
+ targetIdentifierKey: 'videoFeedId',
753
+ relType: { relationshipTag: 'hasFeedChunk', serviceTag: 'VideoFeed' },
754
+ relationshipDirection: 'to'
755
+ }
756
+ ]
757
+ };
758
+
759
+ const source = 'app/src/generatedCode/FlowSchemas/FeedChunkRecordUrl/source';
760
+ const handlerRoot = 'src/generatedCode/FlowSchemas/FeedChunkRecordUrl/source';
761
+
762
+ const result = await emitS3Flow(flowWithRel, source, handlerRoot);
763
+
764
+ const startMain = result.artifacts.find(
765
+ (a) => a.path === `${source}/FeedChunkRecordUrl_Main.js`
766
+ );
767
+ assert.ok(
768
+ startMain.content.includes("requestParams.fieldNames?.videoFeedId"),
769
+ 'Start_Main should extract videoFeedId from requestParams'
770
+ );
771
+ assert.ok(
772
+ startMain.content.includes("['starts-with', '$x-amz-meta-videoFeedId', '']"),
773
+ 'Start_Main should include videoFeedId condition in Presigned POST'
774
+ );
775
+ assert.ok(
776
+ startMain.content.includes("'x-amz-meta-videoFeedId': videoFeedId || ''"),
777
+ 'Start_Main should include videoFeedId field in Presigned POST'
778
+ );
779
+
780
+ const procMain = result.artifacts.find(
781
+ (a) => a.path === `${source}/FeedChunkRecordUrlProcessAfterUpload_Main.js`
782
+ );
783
+ assert.ok(
784
+ procMain.content.includes("const metaVideoFeedId = s3Metadata.videofeedid || '';"),
785
+ 'ProcessAfterUpload_Main should extract metaVideoFeedId from s3Metadata'
786
+ );
787
+ assert.ok(
788
+ procMain.content.includes("videoFeedId: metaVideoFeedId"),
789
+ 'ProcessAfterUpload_Main should include videoFeedId in extracted metadata log'
790
+ );
791
+ assert.ok(
792
+ procMain.content.includes("objectType: 'videoFeed'"),
793
+ 'ProcessAfterUpload_Main should include targetObjType objectType'
794
+ );
795
+ assert.ok(
796
+ procMain.content.includes("relationshipTag: 'hasFeedChunk'"),
797
+ 'ProcessAfterUpload_Main should include relType relationshipTag'
798
+ );
799
+ assert.ok(
800
+ procMain.content.includes("relationshipDirection: 'to'"),
801
+ 'ProcessAfterUpload_Main must generate relationshipDirection to'
802
+ );
803
+ assert.ok(
804
+ procMain.content.includes('Built relationship for videoFeed'),
805
+ 'ProcessAfterUpload_Main should log relationship construction'
806
+ );
807
+ });
808
+
809
+ test('emitS3Flow resolves relationships from schemas with relationshipDirection to', async () => {
810
+ const flow = {
811
+ flowTag: 'FeedChunkRecordUrl',
812
+ initiateBy: 'user',
813
+ handleObj: 'one',
814
+ statusType: 'statusField',
815
+ event: ['s3'],
816
+ objType: {
817
+ objectType: 'feedChunk',
818
+ serviceTag: 'FeedChunk'
819
+ },
820
+ bucketName: 'video-record-temp',
821
+ flowSteps: [
822
+ { stepName: 'Start', event: ['wbs'] },
823
+ { stepName: 'ProcessAfterUpload', event: ['s3'] },
824
+ { stepName: 'ConfirmComplete', event: ['extTopic'] }
825
+ ]
826
+ };
827
+
828
+ const relationshipsSchema = [
829
+ {
830
+ objectType: 'feedChunk',
831
+ relationshipTag: 'hasFeedChunk',
832
+ relationshipServiceTag: 'VideoFeed'
833
+ }
834
+ ];
835
+
836
+ const source = 'app/src/generatedCode/FlowSchemas/FeedChunkRecordUrl/source';
837
+ const handlerRoot = 'src/generatedCode/FlowSchemas/FeedChunkRecordUrl/source';
838
+
839
+ const result = await emitS3Flow(flow, source, handlerRoot, {
840
+ relationships: relationshipsSchema
841
+ });
842
+
843
+ const procMain = result.artifacts.find(
844
+ (a) => a.path === `${source}/FeedChunkRecordUrlProcessAfterUpload_Main.js`
845
+ );
846
+ assert.ok(
847
+ procMain.content.includes("relationshipDirection: 'to'"),
848
+ 'ProcessAfterUpload_Main should infer relationshipDirection to from RefObjectRelationships'
849
+ );
850
+ assert.ok(
851
+ procMain.content.includes("relationshipTag: 'hasFeedChunk'"),
852
+ 'ProcessAfterUpload_Main should infer relationshipTag hasFeedChunk'
853
+ );
854
+ assert.ok(
855
+ procMain.content.includes("serviceTag: 'VideoFeed'"),
856
+ 'ProcessAfterUpload_Main should infer serviceTag VideoFeed'
857
+ );
858
+ });
859
+
860
+ test('emitS3Flow generates WebSocketTask atomic claim in ProcessAfterUpload and connectionId NONE guard in ConfirmComplete', async () => {
861
+ const flow = {
862
+ flowTag: 'FeedChunkRecordUrl',
863
+ initiateBy: 'user',
864
+ handleObj: 'one',
865
+ statusType: 'statusField',
866
+ event: ['s3'],
867
+ objType: {
868
+ objectType: 'feedChunk',
869
+ serviceTag: 'FeedChunk'
870
+ },
871
+ bucketName: 'video-record-temp',
872
+ flowSteps: [
873
+ { stepName: 'Start', event: ['wbs'] },
874
+ { stepName: 'ProcessAfterUpload', event: ['s3'] },
875
+ { stepName: 'ConfirmComplete', event: ['extTopic'] }
876
+ ]
877
+ };
878
+
879
+ const source = 'app/src/generatedCode/FlowSchemas/FeedChunkRecordUrl/source';
880
+ const handlerRoot = 'src/generatedCode/FlowSchemas/FeedChunkRecordUrl/source';
881
+
882
+ const result = await emitS3Flow(flow, source, handlerRoot);
883
+
884
+ const procMain = result.artifacts.find(
885
+ (a) => a.path === `${source}/FeedChunkRecordUrlProcessAfterUpload_Main.js`
886
+ );
887
+ assert.ok(
888
+ procMain.content.includes('attribute_exists(taskKey) AND attribute_not_exists(uploadClaimed)'),
889
+ 'ProcessAfterUpload_Main must enforce attribute_exists(taskKey) AND attribute_not_exists(uploadClaimed)'
890
+ );
891
+ assert.ok(
892
+ procMain.content.includes("status: 'unauthorized_or_unknown_upload'"),
893
+ 'ProcessAfterUpload_Main must abort when task record is missing'
894
+ );
895
+ assert.ok(
896
+ procMain.content.includes('dynamodbSharedLib'),
897
+ 'ProcessAfterUpload_Main must import dynamodbSharedLib for simplified S3'
898
+ );
899
+
900
+ assert.ok(
901
+ procMain.content.includes("status: 'skipped_empty_or_dir'"),
902
+ 'ProcessAfterUpload_Main must guard against directory markers or 0-byte files'
903
+ );
904
+
905
+ const confirmMain = result.artifacts.find(
906
+ (a) => a.path === `${source}/FeedChunkRecordUrlConfirmComplete_Main.js`
907
+ );
908
+ assert.ok(
909
+ confirmMain.content.includes("taskRecord.connectionId !== 'NONE'"),
910
+ 'ConfirmComplete_Main must guard against connectionId NONE'
911
+ );
912
+ });
913
+
914
+ test('resolveTargetIdentifierInfo resolves both DynamoDB and Graph identifier formats', async () => {
915
+ const { resolveTargetIdentifierInfo } = await import(
916
+ '../../../src/emit/application/custom/customFlowS3.js'
917
+ );
918
+
919
+ // 1. Graph style: { type: 'identifier', fieldName: 'chunkUuid' }
920
+ const graphFlow = { objType: { objectType: 'feedChunk' } };
921
+ const graphObjects = [
922
+ {
923
+ objectType: 'feedChunk',
924
+ identifiers: [{ type: 'identifier', fieldName: 'chunkUuid' }]
925
+ }
926
+ ];
927
+ const graphResult = resolveTargetIdentifierInfo(graphFlow, graphObjects);
928
+ assert.equal(graphResult.identifierKey, 'chunkUuid');
929
+ assert.deepEqual(graphResult.identifierKeys, ['chunkUuid']);
930
+
931
+ // 2. DynamoDB style: { type: 'partitionKey', fieldName: 'myFeedId' }
932
+ const dynamoFlow = { objType: { objectType: 'feedChunk' } };
933
+ const dynamoObjects = [
934
+ {
935
+ objectType: 'feedChunk',
936
+ identifiers: [{ type: 'partitionKey', fieldName: 'myFeedId' }]
937
+ }
938
+ ];
939
+ const dynamoResult = resolveTargetIdentifierInfo(dynamoFlow, dynamoObjects);
940
+ assert.equal(dynamoResult.identifierKey, 'myFeedId');
941
+ assert.deepEqual(dynamoResult.identifierKeys, ['myFeedId']);
942
+
943
+ // 3. Fallback when no identifiers defined
944
+ const emptyFlow = { objType: { objectType: 'videoFeed' } };
945
+ const emptyResult = resolveTargetIdentifierInfo(emptyFlow, []);
946
+ assert.equal(emptyResult.identifierKey, 'videoFeedId');
947
+ assert.deepEqual(emptyResult.identifierKeys, ['videoFeedId']);
948
+ });
949
+
950
+ test('emitS3Flow generates dynamic identifier name for both Start_Main and ProcessAfterUpload_Main', async () => {
951
+ const flow = {
952
+ flowTag: 'FeedChunkRecordUrl',
953
+ flowType: 'custom',
954
+ event: ['s3'],
955
+ bucketName: 'video-record-temp',
956
+ contentTypePrefix: 'video/',
957
+ maxFileSizeMb: 50,
958
+ objType: {
959
+ objectType: 'feedChunk',
960
+ serviceTag: 'FeedChunk'
961
+ },
962
+ flowSteps: [
963
+ { stepName: 'Start', event: ['wbs'] },
964
+ { stepName: 'ProcessAfterUpload', event: ['s3'] },
965
+ { stepName: 'ConfirmComplete', event: ['sqs'] }
966
+ ]
967
+ };
968
+
969
+ const customObjects = [
970
+ {
971
+ objectType: 'feedChunk',
972
+ identifiers: [
973
+ {
974
+ type: 'identifier',
975
+ fieldName: 'customChunkId'
976
+ }
977
+ ]
978
+ }
979
+ ];
980
+
981
+ const source = 'app/src/generatedCode/FlowSchemas/FeedChunkRecordUrl/source';
982
+ const handlerRoot = 'src/generatedCode/FlowSchemas/FeedChunkRecordUrl/source';
983
+
984
+ const result = await emitS3Flow(flow, source, handlerRoot, {
985
+ objects: customObjects
986
+ });
987
+
988
+ const startMain = result.artifacts.find(
989
+ (a) => a.path === `${source}/FeedChunkRecordUrl_Main.js`
990
+ );
991
+ assert.ok(
992
+ startMain.content.includes('const customChunkId = identifierUuid()'),
993
+ 'Start_Main must bind custom identifier key'
994
+ );
995
+ assert.ok(
996
+ startMain.content.includes("'x-amz-meta-customChunkId': customChunkId"),
997
+ 'Start_Main must use custom identifier key in S3 metadata'
998
+ );
999
+
1000
+ const procMain = result.artifacts.find(
1001
+ (a) => a.path === `${source}/FeedChunkRecordUrlProcessAfterUpload_Main.js`
1002
+ );
1003
+ assert.ok(
1004
+ procMain.content.includes('customChunkId: rawId'),
1005
+ 'ProcessAfterUpload_Main must use custom identifier key in fieldNames and identifiers'
1006
+ );
1007
+ });
1008
+
1009
+
728
1010
 
729
1011
 
730
1012
 
@@ -97,6 +97,10 @@ test('emits queues, topics, tables, and IAM roles from schema data', async () =>
97
97
  dynamoYaml,
98
98
  /RunningTotalReservedUsageTable:\s+Type: 'AWS::DynamoDB::Table'[\s\S]+TableName: \${self:custom\.iz_resourcePrefix}RunningTotalReservedUsage/
99
99
  );
100
+ assert.match(
101
+ dynamoYaml,
102
+ /Outputs:\s+RunningTotalReservedUsageStreamArn:\s+Value:\s+Fn::GetAtt: \[RunningTotalReservedUsageTable, StreamArn\]\s+Export:\s+Name: \${self:custom\.iz_resourcePrefix}RunningTotalReservedUsageStreamArn/
103
+ );
100
104
  const iam = await readFile(
101
105
  join(
102
106
  rootPath,