Tweak GraphQL API Schema
IMO it's very important to have a solid API schema to start with (even if the underlying implementation might still contain some hacks) because it will be very painful to change that lateron. The current GraphQL API Schema is already pretty comprehensive but it has some limitations I would suggest to get rid of before we "release it to the public":
- It uses primitives for many fields/arguments with special semantics
- Scalars are really "cheap" in GraphQL and they allow to add meaning, documentation and (potentially) validation (e.g.
String=>Url)
- Scalars are really "cheap" in GraphQL and they allow to add meaning, documentation and (potentially) validation (e.g.
- It uses types or primitives for a fixed set of values
- GraphQL has the notion of
enumsfor that (e.g.type AssetType=>enum AssetTypeandassetType: String=>assetType: AssetType)
- GraphQL has the notion of
- Some of the fields should be parametrized (e.g.
Asset.thumbnail) - It doesn't use a standard pagination scheme making it hard to scale (offset/limit can get very slow).
- I suggest to implement the Relay GraphQL Cursor Connections Specification that is supported by many client libraries out-of-the box and allow us to switch to a cursor based pagination internally if limit/offset reaches its limit
- This will also render the
*Countqueries obsolete
Here is my first take on an overhaul:
type Query{
"""
a single asset by its id, or null if no corresponding asset exists
"""
asset(
id: AssetId!
): Asset
"""
all assets that are assigned to the given tag
"""
assetsByTag(
tag: Tag!
): Assets!
"""
all asset proxies that match the given criteria
"""
assetProxies(
assetSourceId: AssetSourceId!
tag: Tag
assetCollectionId: AssetCollectionId
assetType: AssetTypeFilter
searchTerm: String
): AssetProxies!
"""
all configured asset sources (by default only the "neos" source)
"""
assetSources: [AssetSource!]!
"""
all asset collections
"""
assetCollections: [AssetCollection!]!
"""
all tags
"""
tags: [Tag!]!
}
"""
An asset (Image, Document, Video or Audio)
"""
type Asset{
id: AssetId!
title: String
label: String!
caption: String
mediaType: MediaType!
fileExtension: FileExtension!
filename: Filename!
copyrightNotice: String
tags: [Tag!]!
collections: [AssetCollection!]!
thumbnail(
maximumWidth: Int
maximumHeight: Int
ratioMode: RatioMode
allowUpScaling: Boolean
): Image
}
"""
A stand-in object of remote or already imported assets from an asset source
"""
type AssetProxy{
id: AssetProxyId!
label: String!
filename: String
lastModified: String!
fileSize: FileSize!
mediaType: MediaType!
fileTypeIcon: Image!
widthInPixels: Int
heightInPixels: Int
thumbnailUri: Url
previewUri: Url
assetSource: AssetSource!
localAssetIdentifier: AssetId
localAsset: Asset
iptcProperties: [IptcProperty!]
iptcProperty(
property: IptcPropertyName
): IptcProperty
}
"""
A collection of assets. One asset can belong to multiple collections
"""
type AssetCollection{
title: AssetCollectionTitle!
assets: Assets!
tags: [Tag!]
}
"""
Asset sources allow to integrate assets from external DAM systems
"""
type AssetSource{
id: AssetSourceId!
label: String!
description: String
iconUri: Url
readOnly: Boolean!
supportsTagging: Boolean!
supportsCollections: Boolean!
}
"""
A list of Assets implementing the Relay Cursor Connections Specification (https://relay.dev/graphql/connections.htm)
"""
type Assets{
pageInfo: PageInfo!
count: Int!
edges: [AssetEdge!]!
}
"""
A single entry in the Assets connection
"""
type AssetEdge{
node: Asset
cursor: String!
}
"""
A list of Asset Proxies implementing the Relay Cursor Connections Specification (https://relay.dev/graphql/connections.htm)
"""
type AssetProxies{
pageInfo: PageInfo!
count: Int!
edges: [AssetProxyEdge!]!
}
"""
A single entry in the AssetProxies connection
"""
type AssetProxyEdge{
node: AssetProxy!
cursor: String!
}
"""
IPTC metadata of an asset that implements the SupportsIptcMetadataInterface (see https://www.iptc.org/))
"""
type IptcProperty{
propertyName: IptcPropertyName!
value: String!
}
"""
GraphQL Cursor Connections Specification PageInfo (https://relay.dev/graphql/connections.htm)
"""
type PageInfo{
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
"""
Representation of an image that can be rendered to the browser
"""
type Image{
width: Int!
height: Int!
url: Url!
alt: String
}
"""
Filter for the type of an asset
"""
enum AssetTypeFilter{
"""
All assets
"""
ALL
"""
Only images (jpg, png, ...)
"""
IMAGE
"""
Only documents (pdf, docx, ...)
"""
DOCUMENT
"""
Only video files (mp4, mov, ...)
"""
VIDEO
"""
Only audio files (mp3, wav, ...)
"""
AUDIO
}
"""
Ratio mode of an image
"""
enum RatioMode{
"""
Inset ratio mode: If an image is attempted to get scaled with the size of both edges stated, using this mode will scale it to the lower of both edges.
Consider an image of 320/480 being scaled to 50/50: because aspect ratio wouldn't get hurt, the target image size will become 33/50.
"""
INSET
"""
Outbound ratio mode: If an image is attempted to get scaled with the size of both edges stated, using this mode will scale the image and crop it.
Consider an image of 320/480 being scaled to 50/50: the image will be scaled to height 50, then centered and cropped so the width will also be 50.
"""
OUTBOUND
}
"""
Unique identifier (UUID) of an Asset
"""
scalar AssetId
"""
Unique identifier of an Asset Proxy
"""
scalar AssetProxyId
"""
Unique identifier of an Asset source (e.g. "neos")
"""
scalar AssetSourceId
"""
Unique identifier of an Asset collection (e.g. "neos")
"""
scalar AssetCollectionId
"""
IANA media type of an Asset (e.g. "image/jpeg")
"""
scalar MediaType
"""
A File extension (e.g. "pdf")
"""
scalar FileExtension
"""
Base file name including extension (e.g. "some-file.pdf")
"""
scalar Filename
"""
A Tag that can be assigned to an Asset
"""
scalar Tag
"""
Size of a file in bytes
"""
scalar FileSize
"""
Name of an IPTC metadata property (e.g. "Creator", see https://www.iptc.org/)
"""
scalar IptcPropertyName
"""
The title of an Asset collection
"""
scalar AssetCollectionTitle
"""
An absolute or relative URL
"""
scalar Url
schema{
query: Query
}
You can see it in "action" here: https://app.graphqleditor.com/flowpack/media-ui?visibleMenu=faker or https://faker.graphqleditor.com/flowpack/media-ui/graphql
Thanks so much!
This is exactly what I hoped for :)
Two things I see right now:
-
Don't Scalars need any type hint? For example to define FileSize correctly.
-
I'm still thinking about hiding
Assetfrom direct querying asAssetProxyist our primary type to work with. Every Asset has an Assetproxy but not every Proxy has an Asset. And I think it's really confusing if you don't know the inner workings of the Media Model.
Don't Scalars need any type hint? For example to define FileSize correctly.
No you can feed it any primitive (e.g. int for the FileSize scalar)
I'm still thinking about hiding Asset from direct querying [...]
Good point. I mainly re-created the current implementation but there is absolutely no need to stick so close to the internal PHP API.
I could even imagine not to expose the term "AssetProxy" at all and instead map the Asset to a mix of AssetProxy and the actual Asset internally..
Yes that's exactly what I had in mind.
I had the Assets in my schema because I initially started with them until I realised they are not the primary element to work with.
I'll happily go through this with you together to come up with a good initial version that we can built upon!
btw: The whole Relay pagination is a bit cumbersome for the implementation and when used "as human", but I think it's worth the extensibility
That would be great :)
FYI: The pagination contains some errors and thinking about it again the query endpoints don't really make sense like this. i.e. there is no way to filter assets by tag and asset source and it should probably be possible to query multiple asset sources at once
Here is a new version after talking to @Sebobo
type Query{
"""
a single asset by its id, or null if no corresponding asset exists
"""
asset(id: AssetId!): Asset
"""
all assets that match the given criteria
"""
assets(
asset_source_id: AssetSourceId!
# multiple tags = all results that match at least one of the given tags (OR)
tags: [Tag!]
assetCollectionId: AssetCollectionId
mediaType: MediaType
searchTerm: String
): AssetConnection!
"""
all configured asset sources (by default only the "neos" source)
"""
assetSources: [AssetSource!]!
"""
all asset collections
"""
assetCollections: [AssetCollection!]!
"""
all tags
"""
tags: [Tag!]!
}
"""
An asset (Image, Document, Video or Audio)
"""
type Asset{
id: AssetId!
assetSource: AssetSource!
label: String!
caption: String
filename: Filename!
tags: [Tag!]!
collections: [AssetCollection!]!
copyrightNotice: String
iptcProperty(property: IptcPropertyName): IptcProperty
lastModified: DateTime
iptcProperties: [IptcProperty!]
# width in pixels (only for Images and Videos)
width: Int
# height in pixels (only for Images and Videos)
height: Int
file: File!
thumbnailUrl: Url
previewUrl: Url
thumbnail(maximumWidth: Int, maximumHeight: Int, ratioMode: RatioMode, allowUpScaling: Boolean): Image
}
"""
The file-representation of an asset including its type and (if available) the URL
"""
type File {
extension: FileExtension!
mediaType: MediaType!
typeIcon: Image!
size: FileSize!
url: Url
}
"""
A collection of assets. One asset can belong to multiple collections
"""
type AssetCollection{
title: AssetCollectionTitle!
assets: AssetConnection!
childCollections: [AssetCollection!]
}
"""
Asset sources allow to integrate assets from external DAM systems
"""
type AssetSource{
id: AssetSourceId!
label: String!
description: String
iconUri: Url
readOnly: Boolean!
supportsTagging: Boolean!
supportsCollections: Boolean!
}
"""
A list of Assets implementing the Relay Cursor Connections Specification (https://relay.dev/graphql/connections.htm)
"""
type AssetConnection{
pageInfo: PageInfo!
edges: [AssetEdge]
}
"""
A single entry in the AssetConnection
"""
type AssetEdge{
node: Asset!
cursor: String!
}
"""
IPTC metadata of an asset that implements the SupportsIptcMetadataInterface (see https://www.iptc.org/))
"""
type IptcProperty{
propertyName: IptcPropertyName!
value: String!
}
"""
GraphQL Cursor Connections Specification PageInfo (https://relay.dev/graphql/connections.htm)
"""
type PageInfo{
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String!
endCursor: String!
}
"""
Representation of an image that can be rendered to the browser
"""
type Image{
width: Int!
height: Int!
url: Url!
alt: String
}
"""
Ratio mode of an image
"""
enum RatioMode{
"""
Inset ratio mode: If an image is attempted to get scaled with the size of both edges stated, using this mode will scale it to the lower of both edges.
Consider an image of 320/480 being scaled to 50/50: because aspect ratio wouldn't get hurt, the target image size will become 33/50.
"""
INSET
"""
Outbound ratio mode: If an image is attempted to get scaled with the size of both edges stated, using this mode will scale the image and crop it.
Consider an image of 320/480 being scaled to 50/50: the image will be scaled to height 50, then centered and cropped so the width will also be 50.
"""
OUTBOUND
}
"""
Unique identifier (UUID) of an Asset
"""
scalar AssetId
"""
Unique identifier of an Asset source (e.g. "neos")
"""
scalar AssetSourceId
"""
Unique identifier of an Asset collection (e.g. "neos")
"""
scalar AssetCollectionId
"""
IANA media type of an Asset (e.g. "image/jpeg")
"""
scalar MediaType
"""
A File extension (e.g. "pdf")
"""
scalar FileExtension
"""
Base file name including extension (e.g. "some-file.pdf")
"""
scalar Filename
"""
A Tag that can be assigned to an Asset
"""
scalar Tag
"""
Size of a file in bytes
"""
scalar FileSize
"""
A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the date-time format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar
"""
scalar DateTime
"""
Name of an IPTC metadata property (e.g. "Creator", see https://www.iptc.org/)
"""
scalar IptcPropertyName
"""
The title of an Asset collection
"""
scalar AssetCollectionTitle
"""
An absolute or relative URL
"""
scalar Url
schema{
query: Query
}
It abstracts the notion of asset proxies and includes some further tweaks.. But it's certainly not the final version :)
Started implementing the Schema.
One thing I noticed is that we need the "imported" flag. It tells me whether I can modify an asset and some other things. Or did we have another idea for that case?
[...] we need the "imported" flag. It tells me whether I can modify an asset
Is external maybe a better name to communicate that?
But isn't an imported asset somehow also still external?
Imported has the actual meaning, that data was transferred at some point.
Isn't every asset imported somehow? :)
But, sure, we don't have to overthink this right now and could always change the names lateron.
So +1 for imported
We planned to add an "import" button so you can already make assets locally available while going through pexels for example. So it fits to that idea.
When I say asset, I mean AssetProxy of course :P
OK. I implemented now almost all of it.
Had to add the localId for imported assets as the Neos UI needs it when selecting a media.
@Sebobo I just had another look at the current schema (after your great presentation *g) and realized that we still use arrays instead of the relay cursor connections. Is there a specific reason for that and/or can I help with that?
Simple time issue. I prioritised the non working things and the arrays worked fine for now. I hoped that you could help me then ;)
Ah cool. Sure, I'll gladly help!