Skip to content

Commit e53430e

Browse files
TatevikGrtatevikg1
andauthored
release: dev → main (add campaign action endpoints, update bounce endpoints) (#168)
* Version: dev * Copy message + patch status endpoints * CampaignActionController * Configure request object normalizer * Datetime form at + test fix * Fix: message format options * Fix: manager configs * Add: testSendMessage endpoint * Client docs workflow * Remove text_message from campaign request * Remove html_formated from campaign request * Add: validateNoClickTrackLinks * Filter campaigns by subject Fix: flush entity manager after updates * Fix: double base64 encoding in TemplateImageNormalizer * Add: update template endpoint * Add: UniqueTemplateTitleValidator * Add: Default templates endpoints * Bounce controller * Bounce controller: add status query parameter to get bounces * Bounce controller: by campaign/subscriber endpoints * BounceRegex controller: create/update rule * Fix test --------- Co-authored-by: Tatevik <tatevikg1@gmail.com>
1 parent 3af507f commit e53430e

53 files changed

Lines changed: 2014 additions & 336 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/client-docs.yml

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,24 @@ name: Update phplist-api-client OpenAPI
33
on:
44
push:
55
branches:
6-
- main
6+
- '**'
77
pull_request:
88

99
jobs:
1010
generate-openapi:
1111
runs-on: ubuntu-22.04
12+
outputs:
13+
source_branch: ${{ steps.branch.outputs.source_branch }}
1214
steps:
15+
- name: Determine source branch
16+
id: branch
17+
run: |
18+
if [ "${{ github.event_name }}" = "pull_request" ]; then
19+
echo "source_branch=${{ github.head_ref }}" >> "$GITHUB_OUTPUT"
20+
else
21+
echo "source_branch=${{ github.ref_name }}" >> "$GITHUB_OUTPUT"
22+
fi
23+
1324
- name: Checkout Source Repository
1425
uses: actions/checkout@v3
1526

@@ -42,6 +53,8 @@ jobs:
4253
update-phplist-api-client:
4354
runs-on: ubuntu-22.04
4455
needs: generate-openapi
56+
env:
57+
TARGET_BRANCH: ${{ needs.generate-openapi.outputs.source_branch }}
4558
steps:
4659
- name: Checkout phplist-api-client Repository
4760
uses: actions/checkout@v3
@@ -50,6 +63,17 @@ jobs:
5063
token: ${{ secrets.PUSH_API_CLIENT }}
5164
fetch-depth: 0
5265

66+
- name: Prepare target branch
67+
run: |
68+
git fetch origin
69+
70+
if git ls-remote --exit-code --heads origin "$TARGET_BRANCH" >/dev/null 2>&1; then
71+
git checkout "$TARGET_BRANCH"
72+
git pull --rebase origin "$TARGET_BRANCH"
73+
else
74+
git checkout -b "$TARGET_BRANCH"
75+
fi
76+
5377
- name: Download Generated OpenAPI JSON
5478
uses: actions/download-artifact@v4
5579
with:
@@ -63,24 +87,31 @@ jobs:
6387
if [ -f openapi.json ]; then
6488
diff openapi.json new-openapi/latest-restapi.json > openapi-diff.txt || true
6589
if [ -s openapi-diff.txt ]; then
66-
echo "diff=true" >> $GITHUB_OUTPUT
90+
echo "diff=true" >> "$GITHUB_OUTPUT"
6791
else
68-
echo "diff=false" >> $GITHUB_OUTPUT
92+
echo "diff=false" >> "$GITHUB_OUTPUT"
6993
fi
7094
else
7195
echo "No previous openapi.json, will add."
72-
echo "diff=true" >> $GITHUB_OUTPUT
96+
echo "diff=true" >> "$GITHUB_OUTPUT"
7397
fi
7498
7599
- name: Update and Commit OpenAPI File
76100
if: steps.diff.outputs.diff == 'true'
77101
run: |
102+
set -euo pipefail
78103
cp new-openapi/latest-restapi.json openapi.json
79104
git config user.name "github-actions"
80105
git config user.email "github-actions@phplist-api-client.workflow"
81106
git add openapi.json
82-
git commit -m "Update openapi.json from REST API workflow `date`"
83-
git push
107+
if git diff --cached --quiet; then
108+
echo "No changes to commit"
109+
exit 0
110+
fi
111+
git commit -m "Update openapi.json from REST API workflow $(date -u +"%Y-%m-%dT%H:%M:%SZ")"
112+
git fetch origin "$TARGET_BRANCH"
113+
git rebase "origin/$TARGET_BRANCH"
114+
git push origin HEAD:"$TARGET_BRANCH"
84115
85116
- name: Skip Commit if No Changes
86117
if: steps.diff.outputs.diff == 'false'

config/services/managers.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ services:
88
autowire: true
99
autoconfigure: true
1010

11-
PhpList\Core\Domain\Identity\Service\SessionManager:
11+
PhpList\Core\Domain\Identity\Service\Manager\SessionManager:
1212
autowire: true
1313
autoconfigure: true
1414

@@ -36,7 +36,7 @@ services:
3636
autowire: true
3737
autoconfigure: true
3838

39-
PhpList\Core\Domain\Identity\Service\AdministratorManager:
39+
PhpList\Core\Domain\Identity\Service\Manager\AdministratorManager:
4040
autowire: true
4141
autoconfigure: true
4242

@@ -56,6 +56,6 @@ services:
5656
autowire: true
5757
autoconfigure: true
5858

59-
PhpList\Core\Domain\Identity\Service\PasswordManager:
59+
PhpList\Core\Domain\Identity\Service\Manager\PasswordManager:
6060
autowire: true
6161
autoconfigure: true
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
services:
2-
PhpList\Core\Domain\Messaging\MessageHandler\CampaignProcessorMessageHandler:
2+
PhpList\Core\Domain\Messaging\MessageHandler\CampaignProcessor\CampaignProcessorMessageHandler:
3+
autowire: true
4+
autoconfigure: true
5+
public: false
6+
7+
PhpList\Core\Domain\Messaging\MessageHandler\CampaignProcessor\TestCampaignProcessorMessageHandler:
38
autowire: true
49
autoconfigure: true
510
public: false

config/services/normalizers.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,11 @@ services:
1515
$classMetadataFactory: '@?serializer.mapping.class_metadata_factory'
1616
$nameConverter: '@Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter'
1717

18+
phplist.request_serializer:
19+
class: Symfony\Component\Serializer\Serializer
20+
arguments:
21+
$normalizers:
22+
- '@Symfony\Component\Serializer\Normalizer\ObjectNormalizer'
23+
1824
PhpList\RestBundle\:
1925
resource: '../../src/*/Serializer/*'

config/services/validators.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
services:
22
PhpList\RestBundle\Common\Validator\RequestValidator:
33
arguments:
4-
$serializer: '@Symfony\Component\Serializer\Normalizer\ObjectNormalizer'
4+
$serializer: '@phplist.request_serializer'
55
$validator: '@validator'
66

77
PhpList\RestBundle\Identity\Validator\Constraint\UniqueEmailValidator:
@@ -27,6 +27,11 @@ services:
2727
PhpList\RestBundle\Messaging\Validator\Constraint\ContainsPlaceholderValidator:
2828
tags: ['validator.constraint_validator']
2929

30+
PhpList\RestBundle\Messaging\Validator\Constraint\UniqueTemplateTitleValidator:
31+
autowire: true
32+
autoconfigure: true
33+
tags: [ 'validator.constraint_validator' ]
34+
3035
PhpList\RestBundle\Identity\Validator\Constraint\UniqueLoginNameValidator:
3136
autowire: true
3237
autoconfigure: true
@@ -50,4 +55,3 @@ services:
5055
autowire: true
5156
autoconfigure: true
5257
tags: [ 'validator.constraint_validator' ]
53-

src/Common/EventListener/ExceptionListener.php

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
1616
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
1717
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
18+
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
1819
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
20+
use Symfony\Component\Validator\Exception\ValidationFailedException;
1921
use Symfony\Component\Validator\Exception\ValidatorException;
2022

2123
class ExceptionListener
@@ -24,7 +26,6 @@ class ExceptionListener
2426
SubscriptionCreationException::class => null,
2527
AttributeDefinitionCreationException::class => null,
2628
AdminAttributeCreationException::class => null,
27-
ValidatorException::class => 400,
2829
AccessDeniedException::class => 403,
2930
AccessDeniedHttpException::class => 403,
3031
AttachmentFileNotFoundException::class => 404,
@@ -36,33 +37,85 @@ public function onKernelException(ExceptionEvent $event): void
3637
{
3738
$exception = $event->getThrowable();
3839

40+
if ($exception instanceof ValidationFailedException
41+
|| $exception instanceof ValidatorException
42+
|| $exception instanceof UnprocessableEntityHttpException
43+
) {
44+
$event->setResponse(
45+
new JsonResponse([
46+
'message' => 'Validation failed',
47+
'errors' => $this->parseFlatValidationMessage($exception->getMessage()),
48+
], 422)
49+
);
50+
51+
return;
52+
}
53+
3954
foreach (self::EXCEPTION_STATUS_MAP as $class => $statusCode) {
4055
if ($exception instanceof $class) {
41-
$status = $statusCode ?? $exception->getStatusCode();
56+
$status = $statusCode ?? (
57+
method_exists($exception, 'getStatusCode')
58+
? $exception->getStatusCode()
59+
: 400
60+
);
61+
4262
$event->setResponse(
4363
new JsonResponse([
44-
'message' => $exception->getMessage()
64+
'message' => $exception->getMessage(),
4565
], $status)
4666
);
67+
4768
return;
4869
}
4970
}
5071

5172
if ($exception instanceof HttpExceptionInterface) {
5273
$event->setResponse(
5374
new JsonResponse([
54-
'message' => $exception->getMessage()
75+
'message' => $exception->getMessage(),
5576
], $exception->getStatusCode())
5677
);
78+
5779
return;
5880
}
5981

6082
if ($exception instanceof Exception) {
6183
$event->setResponse(
6284
new JsonResponse([
63-
'message' => $exception->getMessage()
85+
'message' => $exception->getMessage(),
6486
], 500)
6587
);
6688
}
6789
}
90+
91+
/**
92+
* @return array<string, array<int, string>>
93+
*/
94+
private function parseFlatValidationMessage(string $message): array
95+
{
96+
$errors = [];
97+
$lines = preg_split('/\r\n|\r|\n/', $message) ?: [];
98+
99+
foreach ($lines as $line) {
100+
$line = trim($line);
101+
102+
if ($line === '') {
103+
continue;
104+
}
105+
106+
$parts = explode(':', $line, 2);
107+
108+
if (count($parts) !== 2) {
109+
$errors['_global'][] = $line;
110+
continue;
111+
}
112+
113+
$field = trim($parts[0]);
114+
$errorMessage = trim($parts[1]);
115+
116+
$errors[$field][] = $errorMessage;
117+
}
118+
119+
return $errors;
120+
}
68121
}

src/Common/SwaggerSchemasResponse.php

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,22 @@
66

77
use OpenApi\Attributes as OA;
88

9+
#[OA\Schema(
10+
schema: 'ErrorDetails',
11+
type: 'object',
12+
example: [
13+
'format.formatOptions[0]' => ['The value you selected is not a valid choice.'],
14+
'schedule.repeatUntil' => ['This value is not a valid datetime.'],
15+
'schedule.requeueUntil' => ['This value is not a valid datetime.'],
16+
],
17+
additionalProperties: new OA\AdditionalProperties(
18+
type: 'array',
19+
items: new OA\Items(type: 'string')
20+
)
21+
)]
922
#[OA\Schema(
1023
schema: 'UnauthorizedResponse',
24+
required: ['message'],
1125
properties: [
1226
new OA\Property(
1327
property: 'message',
@@ -19,17 +33,23 @@
1933
)]
2034
#[OA\Schema(
2135
schema: 'ValidationErrorResponse',
36+
required: ['message', 'errors'],
2237
properties: [
2338
new OA\Property(
2439
property: 'message',
2540
type: 'string',
26-
example: 'Some fields are invalid'
41+
example: 'Validation failed'
42+
),
43+
new OA\Property(
44+
property: 'errors',
45+
ref: '#/components/schemas/ErrorDetails'
2746
)
2847
],
2948
type: 'object'
3049
)]
3150
#[OA\Schema(
3251
schema: 'BadRequestResponse',
52+
required: ['message'],
3353
properties: [
3454
new OA\Property(
3555
property: 'message',
@@ -41,6 +61,7 @@
4161
)]
4262
#[OA\Schema(
4363
schema: 'AlreadyExistsResponse',
64+
required: ['message'],
4465
properties: [
4566
new OA\Property(
4667
property: 'message',
@@ -62,7 +83,18 @@
6283
],
6384
type: 'object'
6485
)]
65-
86+
#[OA\Schema(
87+
schema: 'GenericErrorResponse',
88+
required: ['message'],
89+
properties: [
90+
new OA\Property(
91+
property: 'message',
92+
type: 'string',
93+
example: 'An unexpected error occurred.'
94+
)
95+
],
96+
type: 'object'
97+
)]
6698
#[OA\Schema(
6799
schema: 'CursorPagination',
68100
properties: [

src/Common/Validator/RequestValidator.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ public function validate(Request $request, string $dtoClass): RequestInterface
3333
if (isset($routeParams['listId'])) {
3434
$routeParams['listId'] = (int) $routeParams['listId'];
3535
}
36+
if (isset($routeParams['templateId'])) {
37+
$routeParams['templateId'] = (int) $routeParams['templateId'];
38+
}
3639

3740
$data = array_merge($routeParams, $request->query->all(), $body ?? []);
3841

src/Identity/Serializer/AdministratorTokenNormalizer.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace PhpList\RestBundle\Identity\Serializer;
66

7+
use DateTimeInterface;
78
use PhpList\Core\Domain\Identity\Model\AdministratorToken;
89
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
910

@@ -21,7 +22,7 @@ public function normalize($object, string $format = null, array $context = []):
2122
return [
2223
'id' => $object->getId(),
2324
'key' => $object->getKey(),
24-
'expiry_date' => $object->getExpiry()->format('Y-m-d\TH:i:sP'),
25+
'expiry_date' => $object->getExpiry()->format(DateTimeInterface::ATOM),
2526
];
2627
}
2728

0 commit comments

Comments
 (0)