Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support new fields for variables #162

Merged
merged 1 commit into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,788 changes: 1,105 additions & 683 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"homepage": "https://github.com/ChristopheBougere/asl-validator#readme",
"dependencies": {
"ajv": "^8.12.0",
"asl-path-validator": "^0.14.2",
"asl-path-validator": "^0.15.0",
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new version supports variables in JSONPath expressions and also adds a new format type for ResultPath fields which are ReferencePath expressions but cannot contain variables.

"commander": "^10.0.1",
"jsonpath-plus": "^10.0.0",
"yaml": "^2.3.1"
Expand Down
13 changes: 13 additions & 0 deletions src/__tests__/definitions/invalid-jsonata-fields.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"Comment": "QueryLanguage is JSONata but has JSONPath fields",
"StartAt": "EmptyState",
"QueryLanguage": "JSONata",
"States": {
"EmptyState": {
"Type": "Pass",
"Parameters": ["abc"],
"ResultPath": "$.emptyState",
"End": true
}
}
}
106 changes: 106 additions & 0 deletions src/__tests__/definitions/invalid-jsonata-path-fields.asl.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
{
"Comment": "Incorrectly has JSONPath fields on JSONata states",
"StartAt": "Verification",
"States": {
"Verification": {
"Type": "Parallel",
"QueryLanguage": "JSONata",
"Branches": [
{
"StartAt": "Check Identity",
"States": {
"Check Identity": {
"Type": "Pass",
"QueryLanguage": "JSONata",
"End": true,
"OutputPath": "$.foo"
}
}
},
{
"StartAt": "Check Address",
"States": {
"Check Address": {
"Type": "Pass",
"QueryLanguage": "JSONata",
"End": true,
"Output": {
"isAddressValid": "{% $not(null in $each($states.input.data.address, function($v) { $length($trim($v)) > 0 ? $v : null })) %}"
}
}
}
}
],
"Assign": {
"inputPayload": "{% $states.context.Execution.Input %}",
"isCustomerValid": "{% $states.result.isIdentityValid and $states.result.isAddressValid %}"
},
"Next": "Approve or Deny?"
},
"Approve or Deny?": {
"Type": "Choice",
"QueryLanguage": "JSONata",
"Choices": [
{
"Next": "Add Account",
"Condition": "{% $isCustomerValid %}"
}
],
"Default": "Deny Message"
},
"Add Account": {
"Type": "Task",
"QueryLanguage": "JSONata",
"Resource": "arn:aws:states:::dynamodb:putItem",
"Arguments": {
"TableName": "${AccountsTable}",
"Item": {
"PK": {
"S": "{% $uuid() %}"
},
"email": {
"S": "{% $inputPayload.data.identity.email %}"
},
"name": {
"S": "{% $inputPayload.data.firstname & ' ' & $inputPayload.data.lastname %}"
},
"address": {
"S": "{% $join($each($inputPayload.data.address, function($v) { $v }), ', ') %}"
},
"timestamp": {
"S": "{% $now() %}"
}
}
},
"Next": "Home Insurance Interests"
},
"Home Insurance Interests": {
"Type": "Task",
"QueryLanguage": "JSONata",
"Resource": "arn:aws:states:::sqs:sendMessage",
"Arguments": {
"QueueUrl": "${HomeInsuranceInterestQueueArn}",
"MessageBody": "{% ($e := $inputPayload.data.identity.email; $n := $inputPayload.data.firstname & ' ' & $inputPayload.data.lastname; $inputPayload.data.interests[category = 'home']{'customer': $n, 'email': $e, 'totalAssetValue': $sum(estimatedValue), category: {type: yearBuilt}}) %}"
},
"Next": "Approved Message"
},
"Approved Message": {
"Type": "Task",
"Resource": "arn:aws:states:::sns:publish",
"Parameters": {
"TopicArn": "arn:aws:sns:us-east-1:123456789012:CustomerNotifications",
"Message.$": "States.Format('Hello {}, your application has been approved.', $inputPayload.data.firstname)"
},
"End": true
},
"Deny Message": {
"Type": "Task",
"Resource": "arn:aws:states:::sns:publish",
"Parameters": {
"TopicArn": "${SendCustomerNotificationSNSTopicArn}",
"Message.$": "States.Format('Hello {}, your application has been denied because validation of provided data failed', $inputPayload.data.firstname)"
},
"End": true
}
}
}
19 changes: 19 additions & 0 deletions src/__tests__/definitions/invalid-variable-result-path.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"Comment": "ResultPath cannot contain a variable",
"StartAt": "StepOne",
"States": {
"StepOne": {
"Type": "Pass",
"Assign": {
"foo": "bar"
},
"Next": "Fin"
},
"Fin": {
"Type": "Task",
"Resource": "arn:aws:lambda:region-1:1234567890:function:InvalidResultPath",
"ResultPath": "$foo",
"End": true
}
}
}
108 changes: 108 additions & 0 deletions src/__tests__/definitions/valid-jsonata.asl.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
{
"Comment": "This is a state machine for account application service",
"StartAt": "Verification",
"States": {
"Verification": {
"Type": "Parallel",
"QueryLanguage": "JSONata",
"Branches": [
{
"StartAt": "Check Identity",
"States": {
"Check Identity": {
"Type": "Pass",
"QueryLanguage": "JSONata",
"End": true,
"Output": {
"isIdentityValid": "{% $match($states.input.data.identity.email, /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/) and $match($states.input.data.identity.ssn, /^(\\d{3}-?\\d{2}-?\\d{4}|XXX-XX-XXXX)$/) %}"
}
}
}
},
{
"StartAt": "Check Address",
"States": {
"Check Address": {
"Type": "Pass",
"QueryLanguage": "JSONata",
"End": true,
"Output": {
"isAddressValid": "{% $not(null in $each($states.input.data.address, function($v) { $length($trim($v)) > 0 ? $v : null })) %}"
}
}
}
}
],
"Assign": {
"inputPayload": "{% $states.context.Execution.Input %}",
"isCustomerValid": "{% $states.result.isIdentityValid and $states.result.isAddressValid %}"
},
"Next": "Approve or Deny?"
},
"Approve or Deny?": {
"Type": "Choice",
"QueryLanguage": "JSONata",
"Choices": [
{
"Next": "Add Account",
"Condition": "{% $isCustomerValid %}"
}
],
"Default": "Deny Message"
},
"Add Account": {
"Type": "Task",
"QueryLanguage": "JSONata",
"Resource": "arn:aws:states:::dynamodb:putItem",
"Arguments": {
"TableName": "${AccountsTable}",
"Item": {
"PK": {
"S": "{% $uuid() %}"
},
"email": {
"S": "{% $inputPayload.data.identity.email %}"
},
"name": {
"S": "{% $inputPayload.data.firstname & ' ' & $inputPayload.data.lastname %}"
},
"address": {
"S": "{% $join($each($inputPayload.data.address, function($v) { $v }), ', ') %}"
},
"timestamp": {
"S": "{% $now() %}"
}
}
},
"Next": "Home Insurance Interests"
},
"Home Insurance Interests": {
"Type": "Task",
"QueryLanguage": "JSONata",
"Resource": "arn:aws:states:::sqs:sendMessage",
"Arguments": {
"QueueUrl": "${HomeInsuranceInterestQueueArn}",
"MessageBody": "{% ($e := $inputPayload.data.identity.email; $n := $inputPayload.data.firstname & ' ' & $inputPayload.data.lastname; $inputPayload.data.interests[category = 'home']{'customer': $n, 'email': $e, 'totalAssetValue': $sum(estimatedValue), category: {type: yearBuilt}}) %}"
},
"Next": "Approved Message"
},
"Approved Message": {
"Type": "Task",
"Resource": "arn:aws:states:::sns:publish",
"Parameters": {
"TopicArn": "arn:aws:sns:us-east-1:123456789012:CustomerNotifications",
"Message.$": "States.Format('Hello {}, your application has been approved.', $inputPayload.data.firstname)"
},
"End": true
},
"Deny Message": {
"Type": "Task",
"Resource": "arn:aws:states:::sns:publish",
"Parameters": {
"TopicArn": "${SendCustomerNotificationSNSTopicArn}",
"Message.$": "States.Format('Hello {}, your application has been denied because validation of provided data failed', $inputPayload.data.firstname)"
},
"End": true
}
}
}
4 changes: 4 additions & 0 deletions src/checks/json-schema-errors.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Ajv, { ErrorObject } from "ajv";
import paths from "../schemas/paths.json";
import jsonata from "../schemas/jsonata.json";
import choice from "../schemas/choice.json";
import fail from "../schemas/fail.json";
import parallel from "../schemas/parallel.json";
Expand All @@ -20,6 +21,7 @@ export const jsonSchemaErrors: AslChecker = (definition, options) => {
const ajv = new Ajv({
schemas: [
paths,
jsonata,
choice,
fail,
parallel,
Expand All @@ -41,6 +43,8 @@ export const jsonSchemaErrors: AslChecker = (definition, options) => {
ajv.addFormat("asl_path", () => true);
ajv.addFormat("asl_ref_path", () => true);
ajv.addFormat("asl_payload_template", () => true);
// An ASL ResultPath is a ReferencePath that cannot have variables.
ajv.addFormat("asl_result_path", () => true);
}
if (options.checkArn) {
ajv.addFormat("asl_arn", isArnFormatValid);
Expand Down
58 changes: 55 additions & 3 deletions src/checks/state-checks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,17 @@ const getPropertyCount = ({
.reduce((prev, curr) => prev + curr, 0);
};

export const AtMostOne = ({
const enforceMaxCount = ({
props,
errorCode,
path,
maxCount,
errorMessage,
}: {
props: string[];
errorCode: StateMachineErrorCode;
maxCount: number;
errorMessage: string;
// path to a sub-property within the state to use as the
// context for the property checks. This is intended to
// support enforcement of constraints on nested properties
Expand All @@ -106,12 +110,12 @@ export const AtMostOne = ({
return null;
}
const count = getPropertyCount({ object, props });
if (count > 1) {
if (count > maxCount) {
return {
"Error code": errorCode,
// Use of JSONPath within the error message is unnecessary
// since the state names are unique.
Message: `State "${stateName}" MUST contain at most one of ${props
Message: `State "${stateName}" ${errorMessage} ${props
.map((p) => {
return `"${p}"`;
})
Expand All @@ -122,6 +126,54 @@ export const AtMostOne = ({
};
};

export const AtMostOne = ({
props,
errorCode,
path,
}: {
props: string[];
errorCode: StateMachineErrorCode;
// path to a sub-property within the state to use as the
// context for the property checks. This is intended to
// support enforcement of constraints on nested properties
// within a State.
// See the Map's ItemReader.ReaderConfiguration at most one
// rule for MaxItems and MaxItemsPath.
path?: string;
}): StateChecker => {
return enforceMaxCount({
maxCount: 1,
props,
errorCode,
path,
errorMessage: "MUST contain at most one of",
});
};

export const None = ({
props,
errorCode,
path,
}: {
props: string[];
errorCode: StateMachineErrorCode;
// path to a sub-property within the state to use as the
// context for the property checks. This is intended to
// support enforcement of constraints on nested properties
// within a State.
// See the Map's ItemReader.ReaderConfiguration at most one
// rule for MaxItems and MaxItemsPath.
path?: string;
}): StateChecker => {
return enforceMaxCount({
maxCount: 0,
props,
errorCode,
path,
errorMessage: "MUST NOT contain any of",
});
};

export const ExactlyOne = ({
props,
errorCode,
Expand Down
Loading
Loading