diff --git a/CHANGELOG.md b/CHANGELOG.md
index 04f8670f..93917ca2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,10 @@ All notable changes to this project will be documented in this file.
- [#219](https://github.com/os2display/display-api-service/pull/219)
- Fixed psalm, test, coding standards and updated api spec.
+- [#222](https://github.com/os2display/display-api-service/pull/222)
+ - Adds create, update, delete operations to feed-source endpoint.
+ - Adds data validation for feed source.
+
## [2.1.3] - 2024-10-25
- [#220](https://github.com/os2display/display-api-service/pull/220)
diff --git a/config/api_platform/feed_source.yaml b/config/api_platform/feed_source.yaml
index 2a51c034..6375b101 100644
--- a/config/api_platform/feed_source.yaml
+++ b/config/api_platform/feed_source.yaml
@@ -5,9 +5,8 @@ resources:
output: App\Dto\FeedSource
provider: App\State\FeedSourceProvider
processor: App\State\FeedSourceProcessor
-
operations:
- ApiPlatform\Metadata\Get: &get
+ ApiPlatform\Metadata\Get: &ref_0
normalizationContext:
jsonld_embed_context: true
openapiContext:
@@ -20,7 +19,7 @@ resources:
- schema:
type: string
format: ulid
- pattern: "^[A-Za-z0-9]{26}$"
+ pattern: '^[A-Za-z0-9]{26}$'
name: id
in: path
required: true
@@ -29,10 +28,8 @@ resources:
description: OK
content:
application/ld+json:
- examples:
+ examples: null
headers: {}
-
- # https://api-platform.com/docs/core/controllers/
_api_Feed_get_source_config:
class: ApiPlatform\Metadata\Get
method: GET
@@ -49,13 +46,13 @@ resources:
- schema:
type: string
format: ulid
- pattern: "^[A-Za-z0-9]{26}$"
+ pattern: '^[A-Za-z0-9]{26}$'
name: id
in: path
required: true
- schema:
type: string
- pattern: "^[A-Za-z0-9]*$"
+ pattern: '^[A-Za-z0-9]*$'
name: name
in: path
required: true
@@ -66,17 +63,18 @@ resources:
examples:
example1:
value:
- - {key: 'key1', id: 'id1', value: 'value1'}
+ - key: key1
+ id: id1
+ value: value1
headers: {}
-
ApiPlatform\Metadata\GetCollection:
filters:
- - 'entity.search_filter'
- - 'entity.blameable_filter'
- - 'entity.order_filter'
- - 'created.at.order_filter'
- - 'modified.at.order_filter'
- - 'feed_source.search_filter'
+ - entity.search_filter
+ - entity.blameable_filter
+ - entity.order_filter
+ - created.at.order_filter
+ - modified.at.order_filter
+ - feed_source.search_filter
openapiContext:
operationId: get-v2-feed-sources
description: Retrieves a collection of FeedSource resources.
@@ -99,23 +97,108 @@ resources:
description: The number of items per page
- schema:
type: string
- pattern: "^[A-Za-z0-9]*$"
+ pattern: '^[A-Za-z0-9]*$'
name: supportedFeedOutputType
in: query
+ responses:
+ '200':
+ description: OK
+ content:
+ application/ld+json:
+ examples: null
+ headers: {}
+ ApiPlatform\Metadata\Put:
+ security: is_granted("ROLE_ADMIN")
+ openapiContext:
+ description: Update a Feed Source resource.
+ summary: Update a Feed Source resource.
+ operationId: put-v2-feed-source-id
+ tags:
+ - FeedSources
+ parameters:
+ - schema:
+ type: string
+ format: ulid
+ pattern: '^[A-Za-z0-9]{26}$'
+ name: id
+ in: path
+ required: true
+ ApiPlatform\Metadata\Delete:
+ security: is_granted("ROLE_ADMIN")
+ openapiContext:
+ description: Delete a Feed Source resource.
+ summary: Delete a Feed Source resource.
+ operationId: delete-v2-feed-source-id
+ tags:
+ - FeedSources
+ parameters:
+ - schema:
+ type: string
+ format: ulid
+ pattern: '^[A-Za-z0-9]{26}$'
+ name: id
+ in: path
required: true
+ ApiPlatform\Metadata\Post:
+ security: is_granted("ROLE_ADMIN")
+ openapiContext:
+ operationId: create-v2-feed-source
+ description: Creates a Feed Source resource.
+ summary: Creates a Feed Source resource.
+ tags:
+ - FeedSources
+ '_api_/feed_sources/{id}/slides_get': &ref_1
+ normalizationContext:
+ groups:
+ - 'playlist-slide:read'
+ class: ApiPlatform\Metadata\GetCollection
+ method: GET
+ provider: App\State\FeedSourceSlideProvider
+ filters:
+ - entity.search_filter
+ - entity.blameable_filter
+ - App\Filter\PublishedFilter
+ - entity.order_filter
+ - created.at.order_filter
+ - modified.at.order_filter
+ uriTemplate: '/feed-sources/{id}/slides'
+ openapiContext:
+ description: Retrieves collection of weighted slide resources (feedsource).
+ summary: Retrieves collection of weighted slide resources (feedsource).
+ operationId: get-v2-feed-source-slide-id
+ tags:
+ - FeedSources
+ parameters:
+ - schema:
+ type: string
+ format: ulid
+ pattern: '^[A-Za-z0-9]{26}$'
+ name: id
+ in: path
+ required: true
+ - schema:
+ type: integer
+ minimum: 0
+ format: int32
+ default: 1
+ in: query
+ name: page
+ required: true
+ - schema:
+ type: string
+ default: '10'
+ in: query
+ name: itemsPerPage
+ description: The number of items per page
responses:
'200':
description: OK
content:
application/ld+json:
- examples:
+ examples: null
headers: {}
-
- # Our DTO must be a resource to get a proper URL
- # @see https://stackoverflow.com/a/75705084
- # @see https://github.com/api-platform/core/issues/5451
App\Dto\FeedSource:
provider: App\State\FeedSourceProvider
-
operations:
- ApiPlatform\Metadata\Get: *get
+ ApiPlatform\Metadata\Get: *ref_0
+ get_slides: *ref_1
diff --git a/config/services.yaml b/config/services.yaml
index 2b381d94..72293eb9 100644
--- a/config/services.yaml
+++ b/config/services.yaml
@@ -204,6 +204,11 @@ services:
arguments:
$collectionExtensions: !tagged_iterator api_platform.doctrine.orm.query_extension.collection
+ App\State\FeedSourceSlideProvider:
+ tags: [ { name: 'api_platform.state_provider', priority: 2 } ]
+ arguments:
+ $collectionExtensions: !tagged_iterator api_platform.doctrine.orm.query_extension.collection
+
App\State\FeedProvider:
tags: [ { name: 'api_platform.state_provider', priority: 2 } ]
arguments:
diff --git a/psalm-baseline.xml b/psalm-baseline.xml
index 3a079edf..ef8d120f 100644
--- a/psalm-baseline.xml
+++ b/psalm-baseline.xml
@@ -386,17 +386,6 @@
-
-
-
-
-
-
-
-
-
-
-
getId()]]>
diff --git a/public/api-spec-v2.json b/public/api-spec-v2.json
index f35f1980..8f67e25d 100644
--- a/public/api-spec-v2.json
+++ b/public/api-spec-v2.json
@@ -211,11 +211,481 @@
"name": "supportedFeedOutputType",
"in": "query",
"description": "",
+ "required": false,
+ "deprecated": false,
+ "allowEmptyValue": false,
+ "schema": {
+ "type": "string"
+ },
+ "style": "form",
+ "explode": false,
+ "allowReserved": false
+ },
+ {
+ "name": "title",
+ "in": "query",
+ "description": "",
+ "required": false,
+ "deprecated": false,
+ "allowEmptyValue": false,
+ "schema": {
+ "type": "string"
+ },
+ "style": "form",
+ "explode": false,
+ "allowReserved": false
+ },
+ {
+ "name": "description",
+ "in": "query",
+ "description": "",
+ "required": false,
+ "deprecated": false,
+ "allowEmptyValue": false,
+ "schema": {
+ "type": "string"
+ },
+ "style": "form",
+ "explode": false,
+ "allowReserved": false
+ },
+ {
+ "name": "createdBy",
+ "in": "query",
+ "description": "",
+ "required": false,
+ "deprecated": false,
+ "allowEmptyValue": false,
+ "schema": {
+ "type": "string"
+ },
+ "style": "form",
+ "explode": false,
+ "allowReserved": false
+ },
+ {
+ "name": "createdBy[]",
+ "in": "query",
+ "description": "",
+ "required": false,
+ "deprecated": false,
+ "allowEmptyValue": false,
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "style": "form",
+ "explode": true,
+ "allowReserved": false
+ },
+ {
+ "name": "modifiedBy",
+ "in": "query",
+ "description": "",
+ "required": false,
+ "deprecated": false,
+ "allowEmptyValue": false,
+ "schema": {
+ "type": "string"
+ },
+ "style": "form",
+ "explode": false,
+ "allowReserved": false
+ },
+ {
+ "name": "modifiedBy[]",
+ "in": "query",
+ "description": "",
+ "required": false,
+ "deprecated": false,
+ "allowEmptyValue": false,
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "style": "form",
+ "explode": true,
+ "allowReserved": false
+ },
+ {
+ "name": "order[title]",
+ "in": "query",
+ "description": "",
+ "required": false,
+ "deprecated": false,
+ "allowEmptyValue": false,
+ "schema": {
+ "type": "string",
+ "default": "asc",
+ "enum": [
+ "asc",
+ "desc"
+ ]
+ },
+ "style": "form",
+ "explode": false,
+ "allowReserved": false
+ },
+ {
+ "name": "order[description]",
+ "in": "query",
+ "description": "",
+ "required": false,
+ "deprecated": false,
+ "allowEmptyValue": false,
+ "schema": {
+ "type": "string",
+ "default": "asc",
+ "enum": [
+ "asc",
+ "desc"
+ ]
+ },
+ "style": "form",
+ "explode": false,
+ "allowReserved": false
+ },
+ {
+ "name": "order[createdAt]",
+ "in": "query",
+ "description": "",
+ "required": false,
+ "deprecated": false,
+ "allowEmptyValue": false,
+ "schema": {
+ "type": "string",
+ "default": "asc",
+ "enum": [
+ "asc",
+ "desc"
+ ]
+ },
+ "style": "form",
+ "explode": false,
+ "allowReserved": false
+ },
+ {
+ "name": "order[modifiedAt]",
+ "in": "query",
+ "description": "",
+ "required": false,
+ "deprecated": false,
+ "allowEmptyValue": false,
+ "schema": {
+ "type": "string",
+ "default": "asc",
+ "enum": [
+ "asc",
+ "desc"
+ ]
+ },
+ "style": "form",
+ "explode": false,
+ "allowReserved": false
+ },
+ {
+ "name": "supportedFeedOutputType[]",
+ "in": "query",
+ "description": "",
+ "required": false,
+ "deprecated": false,
+ "allowEmptyValue": false,
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "style": "form",
+ "explode": true,
+ "allowReserved": false
+ }
+ ],
+ "deprecated": false
+ },
+ "post": {
+ "operationId": "create-v2-feed-source",
+ "tags": [
+ "FeedSources"
+ ],
+ "responses": {
+ "201": {
+ "description": "FeedSource resource created",
+ "content": {
+ "application/ld+json": {
+ "schema": {
+ "$ref": "#/components/schemas/FeedSource.FeedSource.jsonld"
+ }
+ },
+ "text/html": {
+ "schema": {
+ "$ref": "#/components/schemas/FeedSource.FeedSource"
+ }
+ },
+ "multipart/form-data": {
+ "schema": {
+ "$ref": "#/components/schemas/FeedSource.FeedSource"
+ }
+ }
+ },
+ "links": {}
+ },
+ "400": {
+ "description": "Invalid input"
+ },
+ "422": {
+ "description": "Unprocessable entity"
+ }
+ },
+ "summary": "Creates a Feed Source resource.",
+ "description": "Creates a Feed Source resource.",
+ "parameters": [],
+ "requestBody": {
+ "description": "The new FeedSource resource",
+ "content": {
+ "application/ld+json": {
+ "schema": {
+ "$ref": "#/components/schemas/FeedSource.FeedSourceInput.jsonld"
+ }
+ },
+ "text/html": {
+ "schema": {
+ "$ref": "#/components/schemas/FeedSource.FeedSourceInput"
+ }
+ },
+ "multipart/form-data": {
+ "schema": {
+ "$ref": "#/components/schemas/FeedSource.FeedSourceInput"
+ }
+ }
+ },
+ "required": true
+ },
+ "deprecated": false
+ }
+ },
+ "/v2/feed-sources/{id}": {
+ "get": {
+ "operationId": "get-feed-source-id",
+ "tags": [
+ "FeedSources"
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/ld+json": {
+ "examples": null
+ }
+ },
+ "headers": []
+ }
+ },
+ "summary": "Retrieve a Feed Source resource.",
+ "description": "Retrieves a Feed Source resource.",
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "description": "",
+ "required": true,
+ "deprecated": false,
+ "allowEmptyValue": false,
+ "schema": {
+ "type": "string",
+ "format": "ulid",
+ "pattern": "^[A-Za-z0-9]{26}$"
+ },
+ "style": "simple",
+ "explode": false,
+ "allowReserved": false
+ }
+ ],
+ "deprecated": false
+ },
+ "put": {
+ "operationId": "put-v2-feed-source-id",
+ "tags": [
+ "FeedSources"
+ ],
+ "responses": {
+ "200": {
+ "description": "FeedSource resource updated",
+ "content": {
+ "application/ld+json": {
+ "schema": {
+ "$ref": "#/components/schemas/FeedSource.FeedSource.jsonld"
+ }
+ },
+ "text/html": {
+ "schema": {
+ "$ref": "#/components/schemas/FeedSource.FeedSource"
+ }
+ },
+ "multipart/form-data": {
+ "schema": {
+ "$ref": "#/components/schemas/FeedSource.FeedSource"
+ }
+ }
+ },
+ "links": {}
+ },
+ "400": {
+ "description": "Invalid input"
+ },
+ "422": {
+ "description": "Unprocessable entity"
+ },
+ "404": {
+ "description": "Resource not found"
+ }
+ },
+ "summary": "Update a Feed Source resource.",
+ "description": "Update a Feed Source resource.",
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "description": "",
+ "required": true,
+ "deprecated": false,
+ "allowEmptyValue": false,
+ "schema": {
+ "type": "string",
+ "format": "ulid",
+ "pattern": "^[A-Za-z0-9]{26}$"
+ },
+ "style": "simple",
+ "explode": false,
+ "allowReserved": false
+ }
+ ],
+ "requestBody": {
+ "description": "The updated FeedSource resource",
+ "content": {
+ "application/ld+json": {
+ "schema": {
+ "$ref": "#/components/schemas/FeedSource.FeedSourceInput.jsonld"
+ }
+ },
+ "text/html": {
+ "schema": {
+ "$ref": "#/components/schemas/FeedSource.FeedSourceInput"
+ }
+ },
+ "multipart/form-data": {
+ "schema": {
+ "$ref": "#/components/schemas/FeedSource.FeedSourceInput"
+ }
+ }
+ },
+ "required": true
+ },
+ "deprecated": false
+ },
+ "delete": {
+ "operationId": "delete-v2-feed-source-id",
+ "tags": [
+ "FeedSources"
+ ],
+ "responses": {
+ "204": {
+ "description": "FeedSource resource deleted"
+ },
+ "404": {
+ "description": "Resource not found"
+ }
+ },
+ "summary": "Delete a Feed Source resource.",
+ "description": "Delete a Feed Source resource.",
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "description": "",
+ "required": true,
+ "deprecated": false,
+ "allowEmptyValue": false,
+ "schema": {
+ "type": "string",
+ "format": "ulid",
+ "pattern": "^[A-Za-z0-9]{26}$"
+ },
+ "style": "simple",
+ "explode": false,
+ "allowReserved": false
+ }
+ ],
+ "deprecated": false
+ }
+ },
+ "/v2/feed-sources/{id}/slides": {
+ "get": {
+ "operationId": "get-v2-feed-source-slide-id",
+ "tags": [
+ "FeedSources"
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/ld+json": {
+ "examples": null
+ }
+ },
+ "headers": []
+ }
+ },
+ "summary": "Retrieves collection of weighted slide resources (feedsource).",
+ "description": "Retrieves collection of weighted slide resources (feedsource).",
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "description": "",
"required": true,
"deprecated": false,
"allowEmptyValue": false,
"schema": {
- "type": "string"
+ "type": "string",
+ "format": "ulid",
+ "pattern": "^[A-Za-z0-9]{26}$"
+ },
+ "style": "simple",
+ "explode": false,
+ "allowReserved": false
+ },
+ {
+ "name": "page",
+ "in": "query",
+ "description": "",
+ "required": true,
+ "deprecated": false,
+ "allowEmptyValue": false,
+ "schema": {
+ "type": "integer",
+ "minimum": 0,
+ "format": "int32",
+ "default": 1
+ },
+ "style": "form",
+ "explode": false,
+ "allowReserved": false
+ },
+ {
+ "name": "itemsPerPage",
+ "in": "query",
+ "description": "The number of items per page",
+ "required": false,
+ "deprecated": false,
+ "allowEmptyValue": false,
+ "schema": {
+ "type": "string",
+ "default": "10"
},
"style": "form",
"explode": false,
@@ -312,26 +782,21 @@
"allowReserved": false
},
{
- "name": "order[title]",
+ "name": "published",
"in": "query",
- "description": "",
+ "description": "If true only published content will be shown",
"required": false,
"deprecated": false,
"allowEmptyValue": false,
"schema": {
- "type": "string",
- "default": "asc",
- "enum": [
- "asc",
- "desc"
- ]
+ "type": "boolean"
},
"style": "form",
"explode": false,
"allowReserved": false
},
{
- "name": "order[description]",
+ "name": "order[title]",
"in": "query",
"description": "",
"required": false,
@@ -350,7 +815,7 @@
"allowReserved": false
},
{
- "name": "order[createdAt]",
+ "name": "order[description]",
"in": "query",
"description": "",
"required": false,
@@ -369,7 +834,7 @@
"allowReserved": false
},
{
- "name": "order[modifiedAt]",
+ "name": "order[createdAt]",
"in": "query",
"description": "",
"required": false,
@@ -388,59 +853,21 @@
"allowReserved": false
},
{
- "name": "supportedFeedOutputType[]",
+ "name": "order[modifiedAt]",
"in": "query",
"description": "",
"required": false,
"deprecated": false,
"allowEmptyValue": false,
- "schema": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "style": "form",
- "explode": true,
- "allowReserved": false
- }
- ],
- "deprecated": false
- }
- },
- "/v2/feed-sources/{id}": {
- "get": {
- "operationId": "get-feed-source-id",
- "tags": [
- "FeedSources"
- ],
- "responses": {
- "200": {
- "description": "OK",
- "content": {
- "application/ld+json": {
- "examples": null
- }
- },
- "headers": []
- }
- },
- "summary": "Retrieve a Feed Source resource.",
- "description": "Retrieves a Feed Source resource.",
- "parameters": [
- {
- "name": "id",
- "in": "path",
- "description": "",
- "required": true,
- "deprecated": false,
- "allowEmptyValue": false,
"schema": {
"type": "string",
- "format": "ulid",
- "pattern": "^[A-Za-z0-9]{26}$"
+ "default": "asc",
+ "enum": [
+ "asc",
+ "desc"
+ ]
},
- "style": "simple",
+ "style": "form",
"explode": false,
"allowReserved": false
}
@@ -7758,6 +8185,11 @@
}
}
},
+ "FeedSource-playlist-slide.read": {
+ "type": "object",
+ "description": "",
+ "deprecated": false
+ },
"FeedSource.FeedSource": {
"type": "object",
"description": "",
@@ -7816,19 +8248,16 @@
}
}
},
+ "FeedSource.FeedSource-playlist-slide.read": {
+ "type": "object",
+ "description": "",
+ "deprecated": false
+ },
"FeedSource.FeedSource.jsonld": {
"type": "object",
"description": "",
"deprecated": false,
"properties": {
- "@id": {
- "readOnly": true,
- "type": "string"
- },
- "@type": {
- "readOnly": true,
- "type": "string"
- },
"@context": {
"readOnly": true,
"oneOf": [
@@ -7856,6 +8285,14 @@
}
]
},
+ "@id": {
+ "readOnly": true,
+ "type": "string"
+ },
+ "@type": {
+ "readOnly": true,
+ "type": "string"
+ },
"title": {
"type": "string"
},
@@ -7909,6 +8346,89 @@
}
}
},
+ "FeedSource.FeedSource.jsonld-playlist-slide.read": {
+ "type": "object",
+ "description": "",
+ "deprecated": false,
+ "properties": {
+ "@id": {
+ "readOnly": true,
+ "type": "string"
+ },
+ "@type": {
+ "readOnly": true,
+ "type": "string"
+ }
+ }
+ },
+ "FeedSource.FeedSourceInput": {
+ "type": "object",
+ "description": "",
+ "deprecated": false,
+ "properties": {
+ "title": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "outputType": {
+ "type": "string"
+ },
+ "feedType": {
+ "type": "string"
+ },
+ "secrets": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "feeds": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "supportedFeedOutputType": {
+ "type": "string"
+ }
+ }
+ },
+ "FeedSource.FeedSourceInput.jsonld": {
+ "type": "object",
+ "description": "",
+ "deprecated": false,
+ "properties": {
+ "title": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "outputType": {
+ "type": "string"
+ },
+ "feedType": {
+ "type": "string"
+ },
+ "secrets": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "feeds": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "supportedFeedOutputType": {
+ "type": "string"
+ }
+ }
+ },
"FeedSource.jsonld": {
"type": "object",
"description": "",
@@ -8002,6 +8522,21 @@
}
}
},
+ "FeedSource.jsonld-playlist-slide.read": {
+ "type": "object",
+ "description": "",
+ "deprecated": false,
+ "properties": {
+ "@id": {
+ "readOnly": true,
+ "type": "string"
+ },
+ "@type": {
+ "readOnly": true,
+ "type": "string"
+ }
+ }
+ },
"Media": {
"type": "object",
"description": "",
diff --git a/public/api-spec-v2.yaml b/public/api-spec-v2.yaml
index 88df1d6a..5eeaf4a5 100644
--- a/public/api-spec-v2.yaml
+++ b/public/api-spec-v2.yaml
@@ -149,7 +149,7 @@ paths:
name: supportedFeedOutputType
in: query
description: ''
- required: true
+ required: false
deprecated: false
allowEmptyValue: false
schema:
@@ -312,6 +312,45 @@ paths:
explode: true
allowReserved: false
deprecated: false
+ post:
+ operationId: create-v2-feed-source
+ tags:
+ - FeedSources
+ responses:
+ '201':
+ description: 'FeedSource resource created'
+ content:
+ application/ld+json:
+ schema:
+ $ref: '#/components/schemas/FeedSource.FeedSource.jsonld'
+ text/html:
+ schema:
+ $ref: '#/components/schemas/FeedSource.FeedSource'
+ multipart/form-data:
+ schema:
+ $ref: '#/components/schemas/FeedSource.FeedSource'
+ links: { }
+ '400':
+ description: 'Invalid input'
+ '422':
+ description: 'Unprocessable entity'
+ summary: 'Creates a Feed Source resource.'
+ description: 'Creates a Feed Source resource.'
+ parameters: []
+ requestBody:
+ description: 'The new FeedSource resource'
+ content:
+ application/ld+json:
+ schema:
+ $ref: '#/components/schemas/FeedSource.FeedSourceInput.jsonld'
+ text/html:
+ schema:
+ $ref: '#/components/schemas/FeedSource.FeedSourceInput'
+ multipart/form-data:
+ schema:
+ $ref: '#/components/schemas/FeedSource.FeedSourceInput'
+ required: true
+ deprecated: false
'/v2/feed-sources/{id}':
get:
operationId: get-feed-source-id
@@ -342,6 +381,298 @@ paths:
explode: false
allowReserved: false
deprecated: false
+ put:
+ operationId: put-v2-feed-source-id
+ tags:
+ - FeedSources
+ responses:
+ '200':
+ description: 'FeedSource resource updated'
+ content:
+ application/ld+json:
+ schema:
+ $ref: '#/components/schemas/FeedSource.FeedSource.jsonld'
+ text/html:
+ schema:
+ $ref: '#/components/schemas/FeedSource.FeedSource'
+ multipart/form-data:
+ schema:
+ $ref: '#/components/schemas/FeedSource.FeedSource'
+ links: { }
+ '400':
+ description: 'Invalid input'
+ '422':
+ description: 'Unprocessable entity'
+ '404':
+ description: 'Resource not found'
+ summary: 'Update a Feed Source resource.'
+ description: 'Update a Feed Source resource.'
+ parameters:
+ -
+ name: id
+ in: path
+ description: ''
+ required: true
+ deprecated: false
+ allowEmptyValue: false
+ schema:
+ type: string
+ format: ulid
+ pattern: '^[A-Za-z0-9]{26}$'
+ style: simple
+ explode: false
+ allowReserved: false
+ requestBody:
+ description: 'The updated FeedSource resource'
+ content:
+ application/ld+json:
+ schema:
+ $ref: '#/components/schemas/FeedSource.FeedSourceInput.jsonld'
+ text/html:
+ schema:
+ $ref: '#/components/schemas/FeedSource.FeedSourceInput'
+ multipart/form-data:
+ schema:
+ $ref: '#/components/schemas/FeedSource.FeedSourceInput'
+ required: true
+ deprecated: false
+ delete:
+ operationId: delete-v2-feed-source-id
+ tags:
+ - FeedSources
+ responses:
+ '204':
+ description: 'FeedSource resource deleted'
+ '404':
+ description: 'Resource not found'
+ summary: 'Delete a Feed Source resource.'
+ description: 'Delete a Feed Source resource.'
+ parameters:
+ -
+ name: id
+ in: path
+ description: ''
+ required: true
+ deprecated: false
+ allowEmptyValue: false
+ schema:
+ type: string
+ format: ulid
+ pattern: '^[A-Za-z0-9]{26}$'
+ style: simple
+ explode: false
+ allowReserved: false
+ deprecated: false
+ '/v2/feed-sources/{id}/slides':
+ get:
+ operationId: get-v2-feed-source-slide-id
+ tags:
+ - FeedSources
+ responses:
+ '200':
+ description: OK
+ content:
+ application/ld+json:
+ examples: null
+ headers: []
+ summary: 'Retrieves collection of weighted slide resources (feedsource).'
+ description: 'Retrieves collection of weighted slide resources (feedsource).'
+ parameters:
+ -
+ name: id
+ in: path
+ description: ''
+ required: true
+ deprecated: false
+ allowEmptyValue: false
+ schema:
+ type: string
+ format: ulid
+ pattern: '^[A-Za-z0-9]{26}$'
+ style: simple
+ explode: false
+ allowReserved: false
+ -
+ name: page
+ in: query
+ description: ''
+ required: true
+ deprecated: false
+ allowEmptyValue: false
+ schema:
+ type: integer
+ minimum: 0
+ format: int32
+ default: 1
+ style: form
+ explode: false
+ allowReserved: false
+ -
+ name: itemsPerPage
+ in: query
+ description: 'The number of items per page'
+ required: false
+ deprecated: false
+ allowEmptyValue: false
+ schema:
+ type: string
+ default: '10'
+ style: form
+ explode: false
+ allowReserved: false
+ -
+ name: title
+ in: query
+ description: ''
+ required: false
+ deprecated: false
+ allowEmptyValue: false
+ schema:
+ type: string
+ style: form
+ explode: false
+ allowReserved: false
+ -
+ name: description
+ in: query
+ description: ''
+ required: false
+ deprecated: false
+ allowEmptyValue: false
+ schema:
+ type: string
+ style: form
+ explode: false
+ allowReserved: false
+ -
+ name: createdBy
+ in: query
+ description: ''
+ required: false
+ deprecated: false
+ allowEmptyValue: false
+ schema:
+ type: string
+ style: form
+ explode: false
+ allowReserved: false
+ -
+ name: 'createdBy[]'
+ in: query
+ description: ''
+ required: false
+ deprecated: false
+ allowEmptyValue: false
+ schema:
+ type: array
+ items:
+ type: string
+ style: form
+ explode: true
+ allowReserved: false
+ -
+ name: modifiedBy
+ in: query
+ description: ''
+ required: false
+ deprecated: false
+ allowEmptyValue: false
+ schema:
+ type: string
+ style: form
+ explode: false
+ allowReserved: false
+ -
+ name: 'modifiedBy[]'
+ in: query
+ description: ''
+ required: false
+ deprecated: false
+ allowEmptyValue: false
+ schema:
+ type: array
+ items:
+ type: string
+ style: form
+ explode: true
+ allowReserved: false
+ -
+ name: published
+ in: query
+ description: 'If true only published content will be shown'
+ required: false
+ deprecated: false
+ allowEmptyValue: false
+ schema:
+ type: boolean
+ style: form
+ explode: false
+ allowReserved: false
+ -
+ name: 'order[title]'
+ in: query
+ description: ''
+ required: false
+ deprecated: false
+ allowEmptyValue: false
+ schema:
+ type: string
+ default: asc
+ enum:
+ - asc
+ - desc
+ style: form
+ explode: false
+ allowReserved: false
+ -
+ name: 'order[description]'
+ in: query
+ description: ''
+ required: false
+ deprecated: false
+ allowEmptyValue: false
+ schema:
+ type: string
+ default: asc
+ enum:
+ - asc
+ - desc
+ style: form
+ explode: false
+ allowReserved: false
+ -
+ name: 'order[createdAt]'
+ in: query
+ description: ''
+ required: false
+ deprecated: false
+ allowEmptyValue: false
+ schema:
+ type: string
+ default: asc
+ enum:
+ - asc
+ - desc
+ style: form
+ explode: false
+ allowReserved: false
+ -
+ name: 'order[modifiedAt]'
+ in: query
+ description: ''
+ required: false
+ deprecated: false
+ allowEmptyValue: false
+ schema:
+ type: string
+ default: asc
+ enum:
+ - asc
+ - desc
+ style: form
+ explode: false
+ allowReserved: false
+ deprecated: false
'/v2/feed_sources/{id}/config/{name}':
get:
operationId: get-v2-feed-source-id-config-name
@@ -5462,6 +5793,10 @@ components:
modified:
type: string
format: date-time
+ FeedSource-playlist-slide.read:
+ type: object
+ description: ''
+ deprecated: false
FeedSource.FeedSource:
type: object
description: ''
@@ -5502,17 +5837,15 @@ components:
modified:
type: string
format: date-time
+ FeedSource.FeedSource-playlist-slide.read:
+ type: object
+ description: ''
+ deprecated: false
FeedSource.FeedSource.jsonld:
type: object
description: ''
deprecated: false
properties:
- '@id':
- readOnly: true
- type: string
- '@type':
- readOnly: true
- type: string
'@context':
readOnly: true
oneOf:
@@ -5530,6 +5863,12 @@ components:
- '@vocab'
- hydra
additionalProperties: true
+ '@id':
+ readOnly: true
+ type: string
+ '@type':
+ readOnly: true
+ type: string
title:
type: string
description:
@@ -5565,6 +5904,63 @@ components:
modified:
type: string
format: date-time
+ FeedSource.FeedSource.jsonld-playlist-slide.read:
+ type: object
+ description: ''
+ deprecated: false
+ properties:
+ '@id':
+ readOnly: true
+ type: string
+ '@type':
+ readOnly: true
+ type: string
+ FeedSource.FeedSourceInput:
+ type: object
+ description: ''
+ deprecated: false
+ properties:
+ title:
+ type: string
+ description:
+ type: string
+ outputType:
+ type: string
+ feedType:
+ type: string
+ secrets:
+ type: array
+ items:
+ type: string
+ feeds:
+ type: array
+ items:
+ type: string
+ supportedFeedOutputType:
+ type: string
+ FeedSource.FeedSourceInput.jsonld:
+ type: object
+ description: ''
+ deprecated: false
+ properties:
+ title:
+ type: string
+ description:
+ type: string
+ outputType:
+ type: string
+ feedType:
+ type: string
+ secrets:
+ type: array
+ items:
+ type: string
+ feeds:
+ type: array
+ items:
+ type: string
+ supportedFeedOutputType:
+ type: string
FeedSource.jsonld:
type: object
description: ''
@@ -5628,6 +6024,17 @@ components:
modified:
type: string
format: date-time
+ FeedSource.jsonld-playlist-slide.read:
+ type: object
+ description: ''
+ deprecated: false
+ properties:
+ '@id':
+ readOnly: true
+ type: string
+ '@type':
+ readOnly: true
+ type: string
Media:
type: object
description: ''
diff --git a/src/Entity/Tenant/FeedSource.php b/src/Entity/Tenant/FeedSource.php
index f4dd4372..2c456595 100644
--- a/src/Entity/Tenant/FeedSource.php
+++ b/src/Entity/Tenant/FeedSource.php
@@ -105,4 +105,41 @@ public function setSupportedFeedOutputType(string $supportedFeedOutputType): sel
return $this;
}
+
+ /**
+ * Retrieves the JSON schema for validation.
+ *
+ * @return array The JSON schema definition
+ */
+ public function getSchema(): array
+ {
+ return [
+ '$schema' => 'https://json-schema.org/draft/2020-12/schema',
+ '$id' => 'https://os2display.dk/config-schema.json',
+ 'title' => 'Config file schema',
+ 'description' => 'Schema for defining config files for templates',
+ 'type' => 'object',
+ 'properties' => [
+ 'title' => [
+ 'description' => 'The title of the feed source',
+ 'type' => 'string',
+ 'minLength' => 1,
+ ],
+ 'description' => [
+ 'description' => 'A description of the feed source',
+ 'type' => 'string',
+ 'minLength' => 1,
+ ],
+ 'feedType' => [
+ 'description' => 'The type of the feed source',
+ 'type' => 'string',
+ 'minLength' => 1,
+ ],
+ 'secrets' => [
+ 'type' => 'array',
+ ],
+ ],
+ 'required' => ['title', 'description', 'feedType', 'secrets'],
+ ];
+ }
}
diff --git a/src/Feed/EventDatabaseApiFeedType.php b/src/Feed/EventDatabaseApiFeedType.php
index 77545187..e2d5416c 100644
--- a/src/Feed/EventDatabaseApiFeedType.php
+++ b/src/Feed/EventDatabaseApiFeedType.php
@@ -304,4 +304,23 @@ public function getSupportedFeedOutputType(): string
{
return self::SUPPORTED_FEED_TYPE;
}
+
+ /**
+ * @throws \JsonException
+ */
+ public function getSchema(): array
+ {
+ return [
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
+ 'type' => 'object',
+ 'properties' => [
+ 'host' => [
+ 'type' => 'string',
+ 'format' => 'url',
+ 'pattern' => 'https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-zA-Z0-9()]{2,6}\\b([-a-zA-Z0-9()@:%_\\+~#?&//=]*)',
+ ],
+ ],
+ 'required' => ['host'],
+ ];
+ }
}
diff --git a/src/Feed/FeedTypeInterface.php b/src/Feed/FeedTypeInterface.php
index 2eda1837..93188eab 100644
--- a/src/Feed/FeedTypeInterface.php
+++ b/src/Feed/FeedTypeInterface.php
@@ -60,4 +60,11 @@ public function getRequiredConfiguration(): array;
* @return string
*/
public function getSupportedFeedOutputType(): string;
+
+ /**
+ * Get validation scheme for feed type.
+ *
+ * @return mixed
+ */
+ public function getSchema(): mixed;
}
diff --git a/src/Feed/KobaFeedType.php b/src/Feed/KobaFeedType.php
index d18b26b2..1d37cf42 100644
--- a/src/Feed/KobaFeedType.php
+++ b/src/Feed/KobaFeedType.php
@@ -257,4 +257,12 @@ private function getBookingsFromResource(string $host, string $apikey, string $r
return $response->toArray();
}
+
+ public function getSchema(): array
+ {
+ return [
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
+ 'type' => 'object',
+ ];
+ }
}
diff --git a/src/Feed/NotifiedFeedType.php b/src/Feed/NotifiedFeedType.php
index 5fd3996a..5bf387df 100644
--- a/src/Feed/NotifiedFeedType.php
+++ b/src/Feed/NotifiedFeedType.php
@@ -247,4 +247,18 @@ private function wrapTags(string $input): string
'',
]);
}
+
+ public function getSchema(): array
+ {
+ return [
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
+ 'type' => 'object',
+ 'properties' => [
+ 'token' => [
+ 'type' => 'string',
+ ],
+ ],
+ 'required' => ['token'],
+ ];
+ }
}
diff --git a/src/Feed/RssFeedType.php b/src/Feed/RssFeedType.php
index ed3bf8e7..7d3de686 100644
--- a/src/Feed/RssFeedType.php
+++ b/src/Feed/RssFeedType.php
@@ -136,4 +136,12 @@ public function getSupportedFeedOutputType(): string
{
return self::SUPPORTED_FEED_TYPE;
}
+
+ public function getSchema(): array
+ {
+ return [
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
+ 'type' => 'object',
+ ];
+ }
}
diff --git a/src/Feed/SparkleIOFeedType.php b/src/Feed/SparkleIOFeedType.php
index 5e13bc4f..430d1bf5 100644
--- a/src/Feed/SparkleIOFeedType.php
+++ b/src/Feed/SparkleIOFeedType.php
@@ -284,4 +284,12 @@ private function wrapTags(string $input): string
return $text;
}
+
+ public function getSchema(): array
+ {
+ return [
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
+ 'type' => 'object',
+ ];
+ }
}
diff --git a/src/Repository/FeedSourceRepository.php b/src/Repository/FeedSourceRepository.php
index 46972d1c..85c2fc8f 100644
--- a/src/Repository/FeedSourceRepository.php
+++ b/src/Repository/FeedSourceRepository.php
@@ -5,8 +5,12 @@
namespace App\Repository;
use App\Entity\Tenant\FeedSource;
+use App\Entity\Tenant\Slide;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
+use Doctrine\ORM\EntityManagerInterface;
+use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ManagerRegistry;
+use Symfony\Component\Uid\Ulid;
/**
* @method FeedSource|null find($id, $lockMode = null, $lockVersion = null)
@@ -16,8 +20,25 @@
*/
class FeedSourceRepository extends ServiceEntityRepository
{
- public function __construct(ManagerRegistry $registry)
- {
+ public function __construct(
+ ManagerRegistry $registry,
+ private readonly EntityManagerInterface $entityManager,
+ ) {
parent::__construct($registry, FeedSource::class);
}
+
+ public function getFeedSourceSlideRelationsFromFeedSourceId(Ulid $feedSourceUlid): QueryBuilder
+ {
+ $queryBuilder = $this->entityManager->createQueryBuilder();
+
+ $queryBuilder
+ ->select('s', 'f', 'fs')
+ ->from(Slide::class, 's')
+ ->leftJoin('s.feed', 'f')
+ ->leftJoin('f.feedSource', 'fs')
+ ->where('fs.id = :feedSourceId')
+ ->setParameter('feedSourceId', $feedSourceUlid, 'ulid');
+
+ return $queryBuilder;
+ }
}
diff --git a/src/State/FeedSourceProcessor.php b/src/State/FeedSourceProcessor.php
index 37e6222d..051a3be0 100644
--- a/src/State/FeedSourceProcessor.php
+++ b/src/State/FeedSourceProcessor.php
@@ -4,47 +4,156 @@
namespace App\State;
+use ApiPlatform\Metadata\DeleteOperationInterface;
+use ApiPlatform\Metadata\Exception\InvalidArgumentException;
use ApiPlatform\Metadata\Operation;
+use ApiPlatform\Metadata\Put;
use ApiPlatform\State\ProcessorInterface;
use App\Dto\FeedSourceInput;
+use App\Entity\Interfaces\TenantScopedUserInterface;
use App\Entity\Tenant\FeedSource;
+use App\Exceptions\UnknownFeedTypeException;
+use App\Repository\FeedSourceRepository;
+use App\Service\FeedService;
use Doctrine\ORM\EntityManagerInterface;
+use JsonSchema\Constraints\Factory;
+use JsonSchema\SchemaStorage;
+use JsonSchema\Validator;
+use Symfony\Bundle\SecurityBundle\Security;
+use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
-class FeedSourceProcessor implements ProcessorInterface
+class FeedSourceProcessor extends AbstractProcessor
{
public function __construct(
- private readonly EntityManagerInterface $entityManager,
- ) {}
+ EntityManagerInterface $entityManager,
+ ProcessorInterface $persistProcessor,
+ ProcessorInterface $removeProcessor,
+ private readonly FeedSourceRepository $feedSourceRepository,
+ private readonly FeedService $feedService,
+ private readonly Security $security,
+ ) {
+ parent::__construct($entityManager, $persistProcessor, $removeProcessor);
+ }
- /**
- * {@inheritdoc}
- */
- public function process(mixed $object, Operation $operation, array $uriVariables = [], array $context = [])
+ public function process($data, Operation $operation, array $uriVariables = [], array $context = []): ?object
{
- $entity = $this->fromInput($object, $operation, $uriVariables, $context);
- $this->entityManager->persist($entity);
- $this->entityManager->flush();
+ if ($operation instanceof DeleteOperationInterface) {
+ $queryBuilder = $this->feedSourceRepository->getFeedSourceSlideRelationsFromFeedSourceId($uriVariables['id']);
+ $hasSlides = $queryBuilder->getQuery()->getResult();
+ if ($hasSlides) {
+ throw new ConflictHttpException('This feed source is used by one or more slides and cannot be deleted.');
+ }
+ }
- return $entity;
+ return parent::process($data, $operation, $uriVariables, $context);
}
/**
- * @return T
+ * @throws UnknownFeedTypeException
+ * @throws \JsonException
*/
- protected function fromInput(FeedSourceInput $object, Operation $operation, array $uriVariables, array $context): FeedSource
+ protected function fromInput(mixed $object, Operation $operation, array $uriVariables, array $context): FeedSource
{
- // FIXME Do we really have to do (something like) this to load an existing object into the entity manager?
+ // Set feed source properties
$feedSource = $this->loadPrevious(new FeedSource(), $context);
- /* @var FeedSourceInput $object */
- empty($object->title) ?: $feedSource->setTitle($object->title);
- empty($object->description) ?: $feedSource->setDescription($object->description);
- empty($object->createdBy) ?: $feedSource->setCreatedBy($object->createdBy);
- empty($object->modifiedBy) ?: $feedSource->setModifiedBy($object->modifiedBy);
- empty($object->secrets) ?: $feedSource->setSecrets($object->secrets);
- empty($object->feedType) ?: $feedSource->setFeedType($object->feedType);
- empty($object->supportedFeedOutputType) ?: $feedSource->setSupportedFeedOutputType($object->supportedFeedOutputType);
+ if (!$feedSource instanceof FeedSource) {
+ throw new InvalidArgumentException('object must by of type FeedSource');
+ }
+
+ $this->updateFeedSourceProperties($feedSource, $object);
+
+ // Set tenant
+ $user = $this->security->getUser();
+ if (!$user instanceof TenantScopedUserInterface) {
+ throw new InvalidArgumentException('The user is not a tenant owner.');
+ }
+ $feedSource->setTenant($user->getActiveTenant());
+
+ // Validate feed source
+ $this->validateFeedSource($object, $operation);
return $feedSource;
}
+
+ protected function updateFeedSourceProperties(FeedSource $feedSource, FeedSourceInput $object): void
+ {
+ if (!empty($object->title)) {
+ $feedSource->setTitle($object->title);
+ }
+ if (!empty($object->description)) {
+ $feedSource->setDescription($object->description);
+ }
+ if (!empty($object->createdBy)) {
+ $feedSource->setCreatedBy($object->createdBy);
+ }
+ if (!empty($object->modifiedBy)) {
+ $feedSource->setModifiedBy($object->modifiedBy);
+ }
+ if (!empty($object->secrets)) {
+ $feedSource->setSecrets($object->secrets);
+ }
+ if (!empty($object->feedType)) {
+ $feedSource->setFeedType($object->feedType);
+ }
+ $supportedFeedOutputType = $feedSource->getSupportedFeedOutputType();
+ if (null !== $supportedFeedOutputType) {
+ $feedSource->setSupportedFeedOutputType($supportedFeedOutputType);
+ }
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws UnknownFeedTypeException
+ */
+ private function validateFeedSource(object $object, Operation $operation): void
+ {
+ $validator = $this->prepareValidator();
+
+ // Prepare base feed source validation schema
+ $feedSourceValidationSchema = (new FeedSource())->getSchema();
+
+ // Validate base feed source
+ $this->executeValidation($object, $validator, $feedSourceValidationSchema);
+
+ // Validate dynamic feed type class
+ $feedTypeClassName = $object->feedType;
+ $feedType = $this->feedService->getFeedType($feedTypeClassName);
+
+ $feedTypeValidationSchema = $feedType->getSchema();
+
+ // If updating and secrets are not set, don't validate.
+ if ($operation instanceof Put && empty($object->secrets)) {
+ return;
+ }
+
+ // Validate secrets based on specific feed type.
+ $secrets = (object) $object->secrets;
+ $this->executeValidation($secrets, $validator, $feedTypeValidationSchema);
+ }
+
+ private function prepareValidator(): Validator
+ {
+ $schemaStorage = new SchemaStorage();
+ $feedSourceValidationSchema = (object) (new FeedSource())->getSchema();
+ $schemaStorage->addSchema('file://contentSchema', $feedSourceValidationSchema);
+
+ return new Validator(new Factory($schemaStorage));
+ }
+
+ /**
+ * @throws \JsonException
+ */
+ private function executeValidation(mixed $object, Validator $validator, ?array $schema = null): void
+ {
+ $validator->validate($object, $schema ?? (new FeedSource())->getSchema());
+ if (!$validator->isValid()) {
+ throw new InvalidArgumentException($this->getErrorMessage($validator));
+ }
+ }
+
+ private function getErrorMessage(Validator $validator): string
+ {
+ return $validator->getErrors()[0]['property'].' '.$validator->getErrors()[0]['message'];
+ }
}
diff --git a/src/State/FeedSourceSlideProvider.php b/src/State/FeedSourceSlideProvider.php
new file mode 100644
index 00000000..631a9cdc
--- /dev/null
+++ b/src/State/FeedSourceSlideProvider.php
@@ -0,0 +1,81 @@
+validationUtils->validateUlid($id);
+
+ $queryBuilder = $this->feedSourceRepository->getFeedSourceSlideRelationsFromFeedSourceId($feedSourceUlid);
+
+ foreach ($this->collectionExtensions as $extension) {
+ if ($extension instanceof QueryCollectionExtensionInterface) {
+ $extension->applyToCollection($queryBuilder, $queryNameGenerator, $resourceClass, $operation,
+ $context);
+ }
+ }
+
+ $request = $this->requestStack->getCurrentRequest();
+ $itemsPerPage = $request->query?->get('itemsPerPage') ?? 10;
+ $page = $request->query?->get('page') ?? 1;
+ $firstResult = ((int) $page - 1) * (int) $itemsPerPage;
+ $query = $queryBuilder->getQuery()
+ ->setFirstResult($firstResult)
+ ->setMaxResults((int) $itemsPerPage);
+
+ $doctrinePaginator = new DoctrinePaginator($query);
+
+ return new Paginator($doctrinePaginator);
+ }
+
+ public function toOutput(object $object): SlideDTO
+ {
+ assert($object instanceof Slide);
+ $output = new SlideDTO();
+
+ $id = $object->getId();
+ if (!$id instanceof Ulid) {
+ throw new \RuntimeException('Can\'t assign id as Slide->getId() did not return a Ulid object.');
+ }
+
+ $output->id = $id;
+ $output->title = $object->getTitle();
+
+ return $output;
+ }
+}
diff --git a/tests/Api/FeedSourceTest.php b/tests/Api/FeedSourceTest.php
index f0e1a000..f427db7c 100644
--- a/tests/Api/FeedSourceTest.php
+++ b/tests/Api/FeedSourceTest.php
@@ -4,8 +4,11 @@
namespace App\Tests\Api;
+use App\Entity\Tenant\Feed;
use App\Entity\Tenant\FeedSource;
+use App\Entity\Tenant\Slide;
use App\Tests\AbstractBaseApiTestCase;
+use Symfony\Component\HttpClient\Exception\ClientException;
class FeedSourceTest extends AbstractBaseApiTestCase
{
@@ -48,4 +51,200 @@ public function testGetItem(): void
'@id' => $iri,
]);
}
+
+ public function testCreateFeedSource(): void
+ {
+ $client = $this->getAuthenticatedClient('ROLE_ADMIN');
+
+ $response = $client->request('POST', '/v2/feed-sources', [
+ 'json' => [
+ 'title' => 'Test feed source',
+ 'description' => 'This is a test feed source',
+ 'feedType' => 'App\Feed\EventDatabaseApiFeedType',
+ 'secrets' => [
+ 'host' => 'https://www.test.dk',
+ ],
+ ],
+ 'headers' => [
+ 'Content-Type' => 'application/ld+json',
+ ],
+ ]);
+
+ $this->assertResponseStatusCodeSame(201);
+ $this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8');
+ $this->assertJsonContains([
+ '@context' => '/contexts/FeedSource',
+ '@type' => 'FeedSource',
+ 'title' => 'Test feed source',
+ 'description' => 'This is a test feed source',
+ 'feedType' => 'App\Feed\EventDatabaseApiFeedType',
+ 'secrets' => [
+ 'host' => 'https://www.test.dk',
+ ],
+ 'createdBy' => 'test@example.com',
+ 'modifiedBy' => 'test@example.com',
+ ]);
+ $this->assertMatchesRegularExpression('@^/v\d/[\w-]+/([A-Za-z0-9]{26})$@', $response->toArray()['@id']);
+ }
+
+ public function testCreateFeedSourceWithoutTitle(): void
+ {
+ $client = $this->getAuthenticatedClient('ROLE_ADMIN');
+
+ $this->expectException(ClientException::class);
+
+ $response = $client->request('POST', '/v2/feed-sources', [
+ 'json' => [
+ 'description' => 'This is a test feed source',
+ 'outputType' => 'This is a test output type',
+ 'feedType' => 'This is a test feed type',
+ 'secrets' => [
+ 'test secret',
+ ],
+
+ 'supportedFeedOutputType' => 'Supported feed output type',
+ ],
+ 'headers' => [
+ 'Content-Type' => 'application/ld+json',
+ ],
+ ]);
+ $this->assertMatchesRegularExpression('@^/v\d/[\w-]+/([A-Za-z0-9]{26})$@', $response->toArray()['@id']);
+ }
+
+ public function testCreateFeedSourceWithoutDescription(): void
+ {
+ $client = $this->getAuthenticatedClient('ROLE_ADMIN');
+
+ $this->expectException(ClientException::class);
+
+ $response = $client->request('POST', '/v2/feed-sources', [
+ 'json' => [
+ 'title' => 'Test feed source',
+ 'outputType' => 'This is a test output type',
+ 'feedType' => 'This is a test feed type',
+ 'secrets' => [
+ 'test secret',
+ ],
+
+ 'supportedFeedOutputType' => 'Supported feed output type',
+ ],
+ 'headers' => [
+ 'Content-Type' => 'application/ld+json',
+ ],
+ ]);
+
+ $this->assertMatchesRegularExpression('@^/v\d/[\w-]+/([A-Za-z0-9]{26})$@', $response->toArray()['@id']);
+ }
+
+ public function testCreateFeedSourceWithEventDatabaseFeedTypeWithoutRequiredSecret(): void
+ {
+ $client = $this->getAuthenticatedClient('ROLE_ADMIN');
+
+ $this->expectException(ClientException::class);
+
+ $response = $client->request('POST', '/v2/feed-sources', [
+ 'json' => [
+ 'title' => 'Test feed source',
+ 'outputType' => 'This is a test output type',
+ 'feedType' => 'App\\Feed\\EventDatabaseApiFeedType',
+ 'secrets' => [
+ 'test secret',
+ ],
+
+ 'supportedFeedOutputType' => 'Supported feed output type',
+ ],
+ 'headers' => [
+ 'Content-Type' => 'application/ld+json',
+ ],
+ ]);
+
+ $this->assertMatchesRegularExpression('@^/v\d/[\w-]+/([A-Za-z0-9]{26})$@', $response->toArray()['@id']);
+ }
+
+ public function testUpdateFeedSource(): void
+ {
+ $client = $this->getAuthenticatedClient('ROLE_ADMIN');
+ $iri = $this->findIriBy(FeedSource::class, ['tenant' => $this->tenant, 'title' => 'Test feed source']);
+
+ $client->request('PUT', $iri, [
+ 'json' => [
+ 'title' => 'Updated title',
+ 'description' => 'Updated description',
+ 'outputType' => 'This is a test output type',
+ 'feedType' => 'App\Feed\EventDatabaseApiFeedType',
+ 'secrets' => [
+ ],
+ ],
+ 'headers' => [
+ 'Content-Type' => 'application/ld+json',
+ ],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertJsonContains([
+ '@type' => 'FeedSource',
+ '@id' => $iri,
+ 'title' => 'Updated title',
+ 'description' => 'Updated description',
+ ]);
+ }
+
+ public function testDeleteFeedSource(): void
+ {
+ $client = $this->getAuthenticatedClient('ROLE_ADMIN');
+
+ $response = $client->request('POST', '/v2/feed-sources', [
+ 'json' => [
+ 'title' => 'Test feed source',
+ 'description' => 'This is a test feed source',
+ 'outputType' => 'This is a test output type',
+ 'feedType' => 'App\Feed\EventDatabaseApiFeedType',
+ 'secrets' => [
+ 'host' => 'https://www.test.dk',
+ ],
+ ],
+ 'headers' => [
+ 'Content-Type' => 'application/ld+json',
+ ],
+ ]);
+ $this->assertResponseIsSuccessful();
+
+ $iri = $response->toArray()['@id'];
+ $client->request('DELETE', $iri);
+
+ $this->assertResponseStatusCodeSame(204);
+
+ $ulid = $this->iriHelperUtils->getUlidFromIRI($iri);
+ $this->assertNull(
+ static::getContainer()->get('doctrine')->getRepository(FeedSource::class)->findOneBy(['id' => $ulid])
+ );
+ }
+
+ public function testDeleteFeedSourceInUse(): void
+ {
+ $client = $this->getAuthenticatedClient('ROLE_ADMIN');
+
+ $manager = static::getContainer()->get('doctrine')->getManager();
+
+ $slide = $manager->getRepository(Slide::class)->findOneBy(['title' => 'slide_abc_1']);
+ $this->assertInstanceOf(Slide::class, $slide, 'This test requires the slide titled slide_abc_1 with connected feed and feed source');
+
+ $feed = $slide->getFeed();
+ $this->assertInstanceOf(Feed::class, $feed, 'This test requires a slide with a feed connected');
+
+ $feedSource = $feed->getFeedSource();
+ $this->assertInstanceOf(FeedSource::class, $feedSource, 'This test requires a feed with a feed source connected');
+
+ $feedSourceIri = $this->findIriBy(FeedSource::class, ['id' => $feedSource->getId()]);
+ $client->request('DELETE', $feedSourceIri);
+
+ // Assert that delete request throws an integrity constraint violation error
+ $this->assertResponseStatusCodeSame(409);
+
+ $ulid = $this->iriHelperUtils->getUlidFromIRI($feedSourceIri);
+
+ $this->assertNotNull(
+ $manager->getRepository(FeedSource::class)->findOneBy(['id' => $ulid])
+ );
+ }
}