Merge branch 'next' into permalink

This commit is contained in:
Chi Vinh Le
2018-08-03 15:59:48 +02:00
74 changed files with 3174 additions and 454 deletions
+13
View File
@@ -0,0 +1,13 @@
import { User } from "talk-server/models/user";
export interface CommonContextOptions {
user?: User;
}
export default class CommonContext {
public user?: User;
constructor({ user }: CommonContextOptions) {
this.user = user;
}
}
@@ -0,0 +1,39 @@
import { DirectiveResolverFn } from "graphql-tools";
import CommonContext from "talk-server/graph/common/context";
import { GQLUSER_ROLE } from "talk-server/graph/tenant/schema/__generated__/types";
export interface AuthDirectiveArgs {
roles?: GQLUSER_ROLE[];
userIDField?: string;
}
const auth: DirectiveResolverFn<
Record<string, string | undefined>,
CommonContext
> = (next, src, { roles, userIDField }: AuthDirectiveArgs, { user }) => {
// If there is a user on the request.
if (user) {
// If the role and user owner checks are disabled, then allow them based on
// their authenticated status.
if (!roles && !userIDField) {
return next();
}
// And the user has the expected role.
if (roles && roles.includes(user.role)) {
// Let the request continue.
return next();
}
// Or the item is owned by the specific user.
if (userIDField && src[userIDField] && src[userIDField] === user.id) {
return next();
}
}
// TODO: return better error.
throw new Error("not authorized");
};
export default auth;
@@ -0,0 +1,136 @@
import { Kind } from "graphql";
import { DateTime } from "luxon";
import Cursor from "./cursor";
describe("parseLiteral", () => {
it("parses a date from a string", () => {
expect(
Cursor.parseLiteral({
kind: Kind.STRING,
value: "2018-07-16T18:34:26.744Z",
})
).toBeInstanceOf(Date);
expect(
Cursor.parseLiteral({
kind: Kind.STRING,
value: "this-should-fail",
})
).toEqual(null);
expect(
Cursor.parseLiteral({
kind: Kind.STRING,
value: "",
})
).toEqual(null);
});
it("parses a number from a string", () => {
expect(
Cursor.parseLiteral({
kind: Kind.STRING,
value: "20",
})
).toEqual(20);
expect(
Cursor.parseLiteral({
kind: Kind.STRING,
value: "0",
})
).toEqual(0);
expect(
Cursor.parseLiteral({
kind: Kind.STRING,
value: "null",
})
).toEqual(null);
expect(
Cursor.parseLiteral({
kind: Kind.STRING,
value: "0",
})
).toEqual(0);
});
it("parses a number from a number", () => {
expect(
Cursor.parseLiteral({
kind: Kind.INT,
value: "20",
})
).toEqual(20);
expect(
Cursor.parseLiteral({
kind: Kind.INT,
value: "0",
})
).toEqual(0);
expect(
Cursor.parseLiteral({
kind: Kind.INT,
value: "",
})
).toEqual(null);
});
it("does not parse unknown kinds", () => {
expect(
Cursor.parseLiteral({
kind: Kind.FLOAT,
value: "0.0",
})
).toEqual(null);
});
});
describe("serialize", () => {
it("renders native dates correctly", () => {
const date = new Date();
const expected = date.toISOString();
expect(Cursor.serialize(date)).toEqual(expected);
expect(Cursor.serialize({})).toEqual(null);
});
it("renders luxon dates correctly", () => {
const date = DateTime.fromJSDate(new Date());
const expected = date.toISO();
expect(Cursor.serialize(date)).toEqual(expected);
});
it("renders numbers correctly", () => {
let value = 50;
let expected = "50";
expect(Cursor.serialize(value)).toEqual(expected);
value = 0;
expected = "0";
expect(Cursor.serialize(value)).toEqual(expected);
expect(Cursor.serialize(null)).toEqual(null);
});
});
describe("parseValue", () => {
it("parses the string value of a Date", () => {
const date = new Date();
const expected = date.toISOString();
expect(Cursor.parseValue(expected)).toBeInstanceOf(Date);
});
it("parses the string value of a number", () => {
expect(Cursor.parseValue("0")).toEqual(0);
});
it("handles invalid properties", () => {
expect(Cursor.parseValue(null)).toEqual(null);
expect(Cursor.parseValue(2)).toEqual(null);
});
});
@@ -1,11 +1,15 @@
import { GraphQLScalarType } from "graphql";
import { Kind } from "graphql/language";
import { DateTime } from "luxon";
import { Cursor } from "talk-server/models/connection";
function parseIntegerCursor(value: string): number | null {
try {
const cursor = parseInt(value, 10);
if (isNaN(cursor)) {
return null;
}
return cursor;
} catch (err) {
+4 -1
View File
@@ -1,13 +1,16 @@
import { Db } from "mongodb";
import CommonContext from "talk-server/graph/common/context";
export interface ManagementContextOptions {
db: Db;
}
export default class ManagementContext {
export default class ManagementContext extends CommonContext {
public db: Db;
constructor({ db }: ManagementContextOptions) {
super({});
this.db = db;
}
}
@@ -1,5 +1,5 @@
import Cursor from "../../common/scalars/cursor";
import { GQLResolver } from "talk-server/graph/management/schema/__generated__/types";
export default {
Cursor,
};
const Resolvers: GQLResolver = {};
export default Resolvers;
@@ -1,6 +1,8 @@
import { IResolvers } from "graphql-tools";
import { loadSchema } from "talk-common/graphql";
import resolvers from "talk-server/graph/management/resolvers";
export default function getManagementSchema() {
return loadSchema("management", resolvers);
return loadSchema("management", resolvers as IResolvers);
}
@@ -7,27 +7,22 @@ Time represented as an ISO8601 string.
"""
scalar Time
"""
Cursor represents a paginating cursor.
"""
scalar Cursor
################################################################################
## Tenant
################################################################################
type Tenant {
id: ID!
id: ID!
"""
organizationName is the name of the organization.
"""
organizationName: String
"""
organizationName is the name of the organization.
"""
organizationName: String
"""
organizationContactEmail is the email of the organization.
"""
organizationContactEmail: String
"""
organizationContactEmail is the email of the organization.
"""
organizationContactEmail: String
}
################################################################################
@@ -35,5 +30,5 @@ type Tenant {
################################################################################
type Query {
tenant(id: ID!): Tenant
tenant(id: ID!): Tenant
}
+5 -2
View File
@@ -1,4 +1,5 @@
import { Db } from "mongodb";
import CommonContext from "talk-server/graph/common/context";
import { Tenant } from "talk-server/models/tenant";
import { User } from "talk-server/models/user";
import loaders from "./loaders";
@@ -10,14 +11,16 @@ export interface TenantContextOptions {
user?: User;
}
export default class TenantContext {
export default class TenantContext extends CommonContext {
public loaders: ReturnType<typeof loaders>;
public mutators: ReturnType<typeof mutators>;
public db: Db;
public tenant: Tenant;
public user?: User;
public tenant: Tenant;
constructor({ user, tenant, db }: TenantContextOptions) {
super({ user });
this.tenant = tenant;
this.user = user;
this.loaders = loaders(this);
@@ -1,8 +1,16 @@
import DataLoader from "dataloader";
import TenantContext from "talk-server/graph/tenant/context";
import { Asset, retrieveManyAssets } from "talk-server/models/asset";
import {
Asset,
FindOrCreateAssetInput,
retrieveManyAssets,
} from "talk-server/models/asset";
import { findOrCreate } from "talk-server/services/assets";
export default (ctx: TenantContext) => ({
findOrCreate: (input: FindOrCreateAssetInput) =>
findOrCreate(ctx.db, ctx.tenant, input),
asset: new DataLoader<string, Asset | null>(ids =>
retrieveManyAssets(ctx.db, ctx.tenant.id, ids)
),
@@ -1,18 +1,48 @@
import DataLoader from "dataloader";
import Context from "talk-server/graph/tenant/context";
import {
ConnectionInput,
retrieveAssetConnection,
retrieveMany,
retrieveRepliesConnection,
AssetToCommentsArgs,
CommentToRepliesArgs,
GQLCOMMENT_SORT,
} from "talk-server/graph/tenant/schema/__generated__/types";
import {
retrieveCommentAssetConnection,
retrieveCommentRepliesConnection,
retrieveManyComments,
} from "talk-server/models/comment";
export default (ctx: Context) => ({
comment: new DataLoader((ids: string[]) =>
retrieveMany(ctx.db, ctx.tenant.id, ids)
retrieveManyComments(ctx.db, ctx.tenant.id, ids)
),
forAsset: (assetID: string, input: ConnectionInput) =>
retrieveAssetConnection(ctx.db, ctx.tenant.id, assetID, input),
forParent: (assetID: string, parentID: string, input: ConnectionInput) =>
retrieveRepliesConnection(ctx.db, ctx.tenant.id, assetID, parentID, input),
forAsset: (
assetID: string,
// Apply the graph schema defaults at the loader.
{
first = 10,
orderBy = GQLCOMMENT_SORT.CREATED_AT_DESC,
after,
}: AssetToCommentsArgs
) =>
retrieveCommentAssetConnection(ctx.db, ctx.tenant.id, assetID, {
first,
orderBy,
after,
}),
forParent: (
assetID: string,
parentID: string,
// Apply the graph schema defaults at the loader.
{
first = 10,
orderBy = GQLCOMMENT_SORT.CREATED_AT_DESC,
after,
}: CommentToRepliesArgs
) =>
retrieveCommentRepliesConnection(ctx.db, ctx.tenant.id, assetID, parentID, {
first,
orderBy,
after,
}),
});
@@ -1,9 +1,9 @@
import DataLoader from "dataloader";
import Context from "talk-server/graph/tenant/context";
import { retrieveMany, User } from "talk-server/models/user";
import { retrieveManyUsers, User } from "talk-server/models/user";
export default (ctx: Context) => ({
user: new DataLoader<string, User | null>(ids =>
retrieveMany(ctx.db, ctx.tenant.id, ids)
retrieveManyUsers(ctx.db, ctx.tenant.id, ids)
),
});
@@ -1,12 +1,12 @@
import TenantContext from "talk-server/graph/tenant/context";
import { CreateCommentInput } from "talk-server/graph/tenant/resolvers/mutation";
import { GQLCreateCommentInput } from "talk-server/graph/tenant/schema/__generated__/types";
import { Comment } from "talk-server/models/comment";
import { create } from "talk-server/services/comments";
export default (ctx: TenantContext) => ({
create: (input: CreateCommentInput): Promise<Comment> => {
create: (input: GQLCreateCommentInput): Promise<Comment> => {
// FIXME: remove tenant + user !
return create(ctx.db, ctx.tenant!.id, {
return create(ctx.db, ctx.tenant, {
author_id: ctx.user!.id,
asset_id: input.assetID,
body: input.body,
@@ -1,10 +1,11 @@
import Context from "talk-server/graph/tenant/context";
import { GQLAssetTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
import { Asset } from "talk-server/models/asset";
import { ConnectionInput } from "talk-server/models/comment";
export default {
comments: async (asset: Asset, input: ConnectionInput, ctx: Context) =>
const Asset: GQLAssetTypeResolver<Asset> = {
comments: (asset, input, ctx) =>
ctx.loaders.Comments.forAsset(asset.id, input),
// TODO: implement this.
isClosed: () => false,
};
export default Asset;
@@ -0,0 +1,14 @@
import { GQLAuthIntegrationsTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
import { AuthIntegration, AuthIntegrations } from "talk-server/models/tenant";
const disabled: AuthIntegration = { enabled: false };
const AuthIntegrations: GQLAuthIntegrationsTypeResolver<AuthIntegrations> = {
local: auth => auth.local || disabled,
sso: auth => auth.sso || disabled,
oidc: auth => auth.oidc || disabled,
google: auth => auth.google || disabled,
facebook: auth => auth.facebook || disabled,
};
export default AuthIntegrations;
@@ -0,0 +1,8 @@
import { GQLAuthSettingsTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
import { Auth } from "talk-server/models/tenant";
const AuthSettings: GQLAuthSettingsTypeResolver<Auth> = {
integrations: auth => auth.integrations,
};
export default AuthSettings;
@@ -1,11 +1,12 @@
import Context from "talk-server/graph/tenant/context";
import { Comment, ConnectionInput } from "talk-server/models/comment";
import { GQLCommentTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
import { Comment } from "talk-server/models/comment";
export default {
createdAt: async (comment: Comment, _: any, ctx: Context) =>
comment.created_at,
author: async (comment: Comment, _: any, ctx: Context) =>
const Comment: GQLCommentTypeResolver<Comment> = {
createdAt: comment => comment.created_at,
author: (comment, input, ctx) =>
ctx.loaders.Users.user.load(comment.author_id),
replies: async (comment: Comment, input: ConnectionInput, ctx: Context) =>
replies: (comment, input, ctx) =>
ctx.loaders.Comments.forParent(comment.asset_id, comment.id, input),
};
export default Comment;
@@ -0,0 +1,10 @@
import { GQLFacebookAuthIntegrationTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
import { FacebookAuthIntegration } from "talk-server/models/tenant";
const FacebookAuthIntegration: GQLFacebookAuthIntegrationTypeResolver<
FacebookAuthIntegration
> = {
config: auth => auth,
};
export default FacebookAuthIntegration;
@@ -0,0 +1,10 @@
import { GQLGoogleAuthIntegrationTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
import { GoogleAuthIntegration } from "talk-server/models/tenant";
const GoogleAuthIntegration: GQLGoogleAuthIntegrationTypeResolver<
GoogleAuthIntegration
> = {
config: auth => auth,
};
export default GoogleAuthIntegration;
@@ -1,13 +1,33 @@
import Cursor from "../../common/scalars/cursor";
import Asset from "./asset";
import Comment from "./comment";
import Mutation from "./mutation";
import Query from "./query";
import Cursor from "talk-server/graph/common/scalars/cursor";
import { GQLResolver } from "talk-server/graph/tenant/schema/__generated__/types";
export default {
import Asset from "./asset";
import AuthIntegrations from "./auth_integrations";
import AuthSettings from "./auth_settings";
import Comment from "./comment";
import FacebookAuthIntegration from "./facebook_auth_integration";
import GoogleAuthIntegration from "./google_auth_integration";
import LocalAuthIntegration from "./local_auth_integration";
import Mutation from "./mutation";
import OIDCAuthIntegration from "./oidc_auth_integration";
import Profile from "./profile";
import Query from "./query";
import SSOAuthIntegration from "./sso_auth_integration";
const Resolvers: GQLResolver = {
Asset,
AuthIntegrations,
AuthSettings,
Comment,
FacebookAuthIntegration,
GoogleAuthIntegration,
LocalAuthIntegration,
OIDCAuthIntegration,
SSOAuthIntegration,
Cursor,
Query,
Mutation,
Profile,
Query,
};
export default Resolvers;
@@ -0,0 +1,8 @@
import { GQLLocalAuthIntegrationTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
import { LocalAuthIntegration } from "talk-server/models/tenant";
const LocalAuthIntegration: GQLLocalAuthIntegrationTypeResolver<
LocalAuthIntegration
> = {};
export default LocalAuthIntegration;
@@ -1,23 +1,7 @@
import { ClientMutationProps } from "talk-server/graph/common/resolvers/mutation";
import TenantContext from "talk-server/graph/tenant/context";
import { Comment } from "talk-server/models/comment";
import { GQLMutationTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
export interface CreateCommentInput extends ClientMutationProps {
assetID: string;
parentID?: string;
body: string;
}
export interface CreateCommentPayload extends ClientMutationProps {
comment: Comment;
}
const Mutation = {
createComment: async (
source: void,
input: CreateCommentInput,
ctx: TenantContext
): Promise<CreateCommentPayload> => ({
const Mutation: GQLMutationTypeResolver<void> = {
createComment: async (source, { input }, ctx) => ({
comment: await ctx.mutators.Comment.create(input),
clientMutationId: input.clientMutationId,
}),
@@ -0,0 +1,10 @@
import { GQLOIDCAuthIntegrationTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
import { OIDCAuthIntegration } from "talk-server/models/tenant";
const OIDCAuthIntegration: GQLOIDCAuthIntegrationTypeResolver<
OIDCAuthIntegration
> = {
config: auth => auth,
};
export default OIDCAuthIntegration;
@@ -0,0 +1,21 @@
import { GQLProfileTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
import { Profile } from "talk-server/models/user";
const resolveType: GQLProfileTypeResolver<Profile> = profile => {
switch (profile.type) {
case "local":
return "LocalProfile";
case "oidc":
return "OIDCProfile";
case "sso":
return "SSOProfile";
default:
// TODO: replace with better error.
throw new Error("invalid profile type");
}
};
export default {
__resolveType: resolveType,
};
@@ -1,15 +1,11 @@
import TenantContext from "talk-server/graph/tenant/context";
import { GQLQueryTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
export default {
asset: async (
source: void,
{ id }: { id: string; url: string },
ctx: TenantContext
) => ctx.loaders.Assets.asset.load(id),
comment: async (
source: void,
{ id }: { id: string; url: string },
ctx: TenantContext
) => (id ? ctx.loaders.Comments.comment.load(id) : null),
settings: async (parent: any, args: any, ctx: TenantContext) => ctx.tenant,
const Query: GQLQueryTypeResolver<void> = {
asset: (source, args, ctx) => ctx.loaders.Assets.findOrCreate(args),
comment: (source, { id }, ctx) =>
id ? ctx.loaders.Comments.comment.load(id) : null,
settings: (source, args, ctx) => ctx.tenant,
me: (source, args, ctx) => ctx.user,
};
export default Query;
@@ -0,0 +1,10 @@
import { GQLSSOAuthIntegrationTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
import { SSOAuthIntegration } from "talk-server/models/tenant";
const SSOAuthIntegration: GQLSSOAuthIntegrationTypeResolver<
SSOAuthIntegration
> = {
config: auth => auth,
};
export default SSOAuthIntegration;
+9 -1
View File
@@ -1,6 +1,14 @@
import { attachDirectiveResolvers, IResolvers } from "graphql-tools";
import { loadSchema } from "talk-common/graphql";
import auth from "talk-server/graph/common/directives/auth";
import resolvers from "talk-server/graph/tenant/resolvers";
export default function getTenantSchema() {
return loadSchema("tenant", resolvers);
const schema = loadSchema("tenant", resolvers as IResolvers);
// Attach the directive resolvers.
attachDirectiveResolvers(schema, { auth });
return schema;
}
@@ -1,3 +1,16 @@
################################################################################
## Custom Directives
################################################################################
"""
auth is a directive that will enforce authorization rules on the schema
definition. It will restrict the viewer of the field based on roles or if the
`userIDField` is specified, it will see if the current users ID equals the field
specified. This allows users that own a specific resource (like a comment, or a
flag) see their own content, but restrict it to everyone else.
"""
directive @auth(roles: [USER_ROLE!], userIDField: String) on FIELD_DEFINITION
################################################################################
## Custom Scalar Types
################################################################################
@@ -31,9 +44,9 @@ enum MODERATION_MODE {
}
"""
Wordlist describes all the available wordlists.
WordlistSettings describes all the available wordlists.
"""
type Wordlist {
type WordlistSettings {
"""
banned words will by default reject the comment if it is found.
"""
@@ -45,12 +58,129 @@ type Wordlist {
suspect: [String!]!
}
# Settings stores the global settings for a given installation.
################################################################################
## AuthSettings
################################################################################
##########################
## LocalAuthIntegration
##########################
type LocalAuthIntegration {
enabled: Boolean!
}
##########################
## SSOAuthIntegration
##########################
type SSOAuthIntegrationConfig {
key: String!
"""
displayNameEnable when enabled, will allow Users to set and view their
displayName's.
"""
displayNameEnable: Boolean!
}
type SSOAuthIntegration {
enabled: Boolean!
config: SSOAuthIntegrationConfig @auth(roles: [ADMIN])
}
##########################
## OIDCAuthIntegration
##########################
type OIDCAuthIntegrationConfig {
clientID: String!
clientSecret: String!
authorizationURL: String!
tokenURL: String!
"""
displayNameEnable when enabled, will allow Users to set and view their
displayName's.
"""
displayNameEnable: Boolean!
}
type OIDCAuthIntegrationOptions {
name: String!
}
type OIDCAuthIntegration {
enabled: Boolean!
options: OIDCAuthIntegrationOptions
config: SSOAuthIntegrationConfig @auth(roles: [ADMIN])
}
##########################
## GoogleAuthIntegration
##########################
type GoogleAuthIntegrationConfig {
clientID: String!
clientSecret: String!
}
type GoogleAuthIntegration {
enabled: Boolean!
config: GoogleAuthIntegrationConfig @auth(roles: [ADMIN])
}
##########################
## FacebookAuthIntegration
##########################
type FacebookAuthIntegrationConfig {
clientID: String!
clientSecret: String!
}
type FacebookAuthIntegration {
enabled: Boolean!
config: FacebookAuthIntegrationConfig @auth(roles: [ADMIN])
}
type AuthIntegrations {
local: LocalAuthIntegration!
sso: SSOAuthIntegration!
oidc: OIDCAuthIntegration!
google: GoogleAuthIntegration!
facebook: FacebookAuthIntegration!
}
"""
AuthSettings contains all the settings related to authentication and
authorization.
"""
type AuthSettings {
"""
integrations are the set of configurations for the variations of
authentication solutions.
"""
integrations: AuthIntegrations!
}
################################################################################
## Settings
################################################################################
"""
Settings stores the global settings for a given Tenant.
"""
type Settings {
"""
domain is the domain that is associated with this Tenant.
"""
domain: String @auth(roles: [ADMIN])
"""
moderation is the moderation mode for all Asset's on the site.
"""
moderation: MODERATION_MODE!
moderation: MODERATION_MODE @auth(roles: [ADMIN])
"""
Enables a requirement for email confirmation before a user can login.
@@ -87,13 +217,13 @@ type Settings {
"""
premodLinksEnable will put all comments that contain links into premod.
"""
premodLinksEnable: Boolean!
premodLinksEnable: Boolean @auth(roles: [ADMIN])
"""
autoCloseStream when true will auto close the stream when the `closeTimeout`
amount of seconds have been reached.
"""
autoCloseStream: Boolean!
autoCloseStream: Boolean! @auth(roles: [ADMIN])
"""
customCssUrl is the URL of the custom CSS used to display on the frontend.
@@ -152,18 +282,79 @@ type Settings {
"""
wordlist will return a given list of words.
"""
wordlist: Wordlist!
wordlist: WordlistSettings @auth(roles: [ADMIN, MODERATOR])
"""
domains will return a given list of whitelisted domains.
"""
domains: [String!]!
domains: [String!] @auth(roles: [ADMIN]) @auth(roles: [ADMIN])
"""
auth contains all the settings related to authentication and authorization.
"""
auth: AuthSettings!
}
################################################################################
## User
################################################################################
enum USER_ROLE {
COMMENTER
MODERATOR
ADMIN
}
enum USER_USERNAME_STATUS {
"""
UNSET is used when the username can be changed, and does not necessarily
require moderator action to become active. This can be used when the user
signs up with a social login and has the option of setting their own
username.
"""
UNSET
"""
SET is used when the username has been set for the first time, but cannot
change without the username being rejected by a moderator and that moderator
agreeing that the username should be allowed to change.
"""
SET
"""
APPROVED is used when the username was changed, and subsequently approved by
said moderator.
"""
APPROVED
"""
REJECTED is used when the username was changed, and subsequently rejected by
said moderator.
"""
REJECTED
"""
CHANGED is used after a user has changed their username after it was
rejected.
"""
CHANGED
}
type LocalProfile {
id: String!
}
type OIDCProfile {
id: String!
provider: String!
}
type SSOProfile {
id: String!
}
union Profile = LocalProfile | OIDCProfile | SSOProfile
"""
User is someone that leaves Comments, and logs in.
"""
@@ -176,7 +367,22 @@ type User {
"""
username is the name of the User visible to other Users.
"""
username: String!
username: String
"""
displayName is provided optionally when enabled and available.
"""
displayName: String
"""
profiles is the array of profiles assigned to the user.
"""
profiles: [Profile!] @auth(roles: [ADMIN, MODERATOR], userIDField: "id")
"""
role is the current role of the User.
"""
role: USER_ROLE! @auth(roles: [ADMIN, MODERATOR], userIDField: "id")
}
################################################################################
@@ -391,9 +597,9 @@ type Query {
assets(cursor: Cursor, limit: Int = 10): AssetsConnection
"""
asset is the Asset specified by its ID.
asset is the Asset specified by its ID/URL.
"""
asset(id: ID!): Asset
asset(id: ID, url: String): Asset
"""
me is the current logged in User.
@@ -463,7 +669,7 @@ type Mutation {
"""
createComment will create a Comment as the current logged in User.
"""
createComment(input: CreateCommentInput!): CreateCommentPayload
createComment(input: CreateCommentInput!): CreateCommentPayload @auth
}
################################################################################