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

Tweak GraphQL API Schema #15

Open
bwaidelich opened this issue May 6, 2020 · 18 comments
Open

Tweak GraphQL API Schema #15

bwaidelich opened this issue May 6, 2020 · 18 comments
Milestone

Comments

@bwaidelich
Copy link

bwaidelich commented May 6, 2020

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)
  • It uses types or primitives for a fixed set of values
    • GraphQL has the notion of enums for that (e.g. type AssetType => enum AssetType and assetType: String => assetType: AssetType)
  • 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 *Count queries obsolete
@bwaidelich
Copy link
Author

bwaidelich commented May 6, 2020

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

@Sebobo
Copy link
Member

Sebobo commented May 7, 2020

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 Asset from direct querying as AssetProxy ist 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.

@bwaidelich
Copy link
Author

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..

@Sebobo
Copy link
Member

Sebobo commented May 7, 2020

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.

@bwaidelich
Copy link
Author

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

@Sebobo
Copy link
Member

Sebobo commented May 7, 2020

That would be great :)

@bwaidelich
Copy link
Author

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

@bwaidelich
Copy link
Author

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 :)

@Sebobo
Copy link
Member

Sebobo commented May 28, 2020

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?

@bwaidelich
Copy link
Author

[...] we need the "imported" flag. It tells me whether I can modify an asset

Is external maybe a better name to communicate that?

@Sebobo
Copy link
Member

Sebobo commented May 28, 2020

But isn't an imported asset somehow also still external?
Imported has the actual meaning, that data was transferred at some point.

@bwaidelich
Copy link
Author

bwaidelich commented May 28, 2020

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

@Sebobo
Copy link
Member

Sebobo commented May 28, 2020

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.

@Sebobo
Copy link
Member

Sebobo commented May 28, 2020

When I say asset, I mean AssetProxy of course :P

Sebobo added a commit that referenced this issue May 31, 2020
@Sebobo
Copy link
Member

Sebobo commented Jun 1, 2020

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.

@bwaidelich
Copy link
Author

@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?

@Sebobo
Copy link
Member

Sebobo commented Jun 19, 2020

Simple time issue. I prioritised the non working things and the arrays worked fine for now.
I hoped that you could help me then ;)

@bwaidelich
Copy link
Author

Ah cool. Sure, I'll gladly help!

@Sebobo Sebobo added this to the Alpha Release milestone Jun 22, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants