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

Subscription [WIP] #158

Merged
merged 9 commits into from May 12, 2020
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -87,6 +87,7 @@
"date-fns": "^2.1.0",
"graphql": "^14.5.8",
"graphql-relay": "^0.6.0",
"graphql-subscriptions": "^1.1.0",
chriskalmar marked this conversation as resolved.
Show resolved Hide resolved
"graphql-type-json": "^0.3.0",
"json-shaper": "^1.2.0",
"lodash": "^4.17.10",
Expand Down
2 changes: 1 addition & 1 deletion src/engine/constants.ts
Expand Up @@ -47,6 +47,7 @@ export const entityPropertiesWhitelist: Array<string> = [
'includeUserTracking',
'indexes',
'mutations',
'subscriptions',
'permissions',
'states',
'preProcessor',
Expand Down Expand Up @@ -118,4 +119,3 @@ export const shadowEntityAttributePropertiesWhitelist: Array<string> = [
'primary',
'meta',
];

86 changes: 85 additions & 1 deletion src/engine/entity/Entity.ts
Expand Up @@ -21,6 +21,12 @@ import {
processEntityPermissions,
} from '../permission/Permission';

import {
Subscription,
defaultEntitySubscription,
processEntitySubscriptions,
} from '../subscription/Subscription';

import { DataType, isDataType, DataTypeFunction } from '../datatype/DataType';
import { isStorageType } from '../storage/StorageType';
import { StorageTypeNull } from '../storage/StorageTypeNull';
Expand Down Expand Up @@ -66,6 +72,7 @@ export type EntitySetup = {
// improve typings ?
mutations?: any;
permissions?: any;
subscriptions?: any;
states?: any;
preProcessor?: Function;
postProcessor?: Function;
Expand All @@ -84,6 +91,7 @@ export class Entity {
indexes?: any;
mutations?: any;
permissions?: any;
subscriptions?: any;
states?: any;
preProcessor?: Function;
postProcessor?: Function;
Expand All @@ -95,6 +103,7 @@ export class Entity {
private referencedByEntities: any;
private _indexes: any;
private _mutations: any;
private _subscriptions: any;
private _states: any;
private _permissions: any;
private _defaultPermissions: any;
Expand Down Expand Up @@ -122,6 +131,7 @@ export class Entity {
indexes,
mutations,
permissions,
subscriptions,
states,
preProcessor,
postProcessor,
Expand Down Expand Up @@ -171,6 +181,7 @@ export class Entity {
this.referencedByEntities = [];
this._indexes = indexes;
this._mutations = mutations;
this._subscriptions = subscriptions;
this._states = states;
this._permissions = permissions;
this._preFilters = preFilters;
Expand Down Expand Up @@ -285,7 +296,7 @@ export class Entity {
return this.mutations;
}

getMutationByName(name) {
getMutationByName(name: string) {
const mutations = this.getMutations();

return mutations
Expand Down Expand Up @@ -335,6 +346,62 @@ export class Entity {
return null;
}

_getDefaultSubscriptions() {
const nonSystemAttributeNames = [];

mapOverProperties(this.getAttributes(), (attribute, attributeName) => {
if (!attribute.isSystemAttribute) {
nonSystemAttributeNames.push(attributeName);
}
});

const subscriptions = {};

defaultEntitySubscription.map(defaultSubscription => {
const key = `${defaultSubscription.name}Subscription`;

subscriptions[key] = new Subscription({
name: defaultSubscription.name,
type: defaultSubscription.type,
description: defaultSubscription.description(this.name),
attributes: nonSystemAttributeNames,
});
});

return subscriptions;
}

_processSubscriptions() {
let subscriptions;

if (!this._subscriptions) {
subscriptions = Object.values(this._getDefaultSubscriptions());
} else {
subscriptions = isFunction(this._subscriptions)
? this._subscriptions(this._getDefaultSubscriptions())
: this._subscriptions;
}

return processEntitySubscriptions(this, subscriptions);
}

getSubscriptions() {
if (this.subscriptions) {
return this.subscriptions;
}

this.subscriptions = this._processSubscriptions();
return this.subscriptions;
}

getSubscriptionByName(name: string) {
const subscriptions = this.getSubscriptions();

return subscriptions
? subscriptions.find(subscription => String(subscription) === name)
: null;
}

getStates() {
if (!this._states || this.states) {
return this.states;
Expand Down Expand Up @@ -730,6 +797,22 @@ export class Entity {
}
});
}

if (this.permissions.subscriptions && this.subscriptions) {
this.subscriptions.map(subscription => {
const subscriptionName = subscription.name;
const permission = this.permissions.subscriptions[subscriptionName];

if (permission) {
const descriptionPermissions = generatePermissionDescription(
permission,
);
if (descriptionPermissions) {
subscription.description += descriptionPermissions;
}
}
});
}
}
}

Expand Down Expand Up @@ -758,6 +841,7 @@ export class Entity {
}

this.getMutations();
this.getSubscriptions();
this.permissions = this._processPermissions();
this._generatePermissionDescriptions();
return this.permissions;
Expand Down
44 changes: 43 additions & 1 deletion src/engine/permission/Permission.spec.ts
Expand Up @@ -906,9 +906,13 @@ describe('Permission', () => {
mutations: {
update: new Permission().role('manager'),
},
subscriptions: {
onUpdate: new Permission().role('manager'),
},
};

processEntityPermissions(entity, permissions);
const permissionMap = processEntityPermissions(entity, permissions);
expect(permissionMap).toMatchSnapshot();
});

it('should throw if provided with an invalid map of permissions', () => {
Expand All @@ -933,6 +937,18 @@ describe('Permission', () => {
expect(fn).toThrowErrorMatchingSnapshot();
});

it('should throw if provided with an invalid map of subscription permissions', () => {
const permissions = {
subscriptions: ['bad'],
};

function fn() {
processEntityPermissions(entity, permissions);
}

expect(fn).toThrowErrorMatchingSnapshot();
});

it('should throw if provided with an invalid permissions', () => {
const permissions1 = {
read: ['bad'],
Expand Down Expand Up @@ -1001,6 +1017,18 @@ describe('Permission', () => {
}

expect(fn3).toThrowErrorMatchingSnapshot();

const permissions4 = {
subscriptions: {
onUpdate: new Permission().userAttribute('notHere'),
},
};

function fn4() {
processEntityPermissions(entity, permissions4);
}

expect(fn4).toThrowErrorMatchingSnapshot();
});

it('should throw if permissions have invalid attributes defined', () => {
Expand Down Expand Up @@ -1029,6 +1057,20 @@ describe('Permission', () => {
expect(fn).toThrowErrorMatchingSnapshot();
});

it('should throw if permissions are assigned to unknown subscriptions', () => {
const permissions = {
subscriptions: {
noSuchSubscription: new Permission().userAttribute('someAttribute'),
},
};

function fn() {
processEntityPermissions(entity, permissions);
}

expect(fn).toThrowErrorMatchingSnapshot();
});

it('should throw if permission is used on a create type mutation and using data-bound permission types', () => {
const permissions1 = {
mutations: {
Expand Down
93 changes: 93 additions & 0 deletions src/engine/permission/Permission.ts
Expand Up @@ -10,6 +10,11 @@ import {
isMutation,
Mutation,
} from '../mutation/Mutation';
import {
SUBSCRIPTION_TYPE_CREATE,
// isSubscription,
Subscription,
} from '../subscription/Subscription';
import { isDataTypeState } from '../datatype/DataTypeState';

/*
Expand All @@ -29,6 +34,7 @@ export type PermissionMap = {
read?: Permission | Permission[];
find?: Permission | Permission[];
mutations?: {} | Permission | Permission[];
subscriptions?: {} | Permission | Permission[];
};

export class Permission {
Expand Down Expand Up @@ -973,6 +979,28 @@ const validatePermissionMutationTypes = (
}
};

const validatePermissionSubscriptionTypes = (
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I copied dumbly the check made for permission.mutation not sure it's needed for subscription.

entity: Entity,
permissions: Permission | Permission[],
subscription: Subscription,
): void => {
if (subscription.type === SUBSCRIPTION_TYPE_CREATE) {
const permissionsArray = isArray(permissions as Permission[])
? (permissions as Permission[])
: ([permissions] as Permission[]);

permissionsArray.map(permission => {
passOrThrow(
!permission.userAttributes.length &&
!permission.states.length &&
!permission.values.length,
() =>
`Create type subscription permission '${subscription.name}' in '${entity.name}.permissions' can only be of type 'authenticated', 'everyone', 'role' or 'lookup'`,
);
});
}
};

export const hasEmptyPermissions = (
_permissions: Permission | Permission[],
): boolean => {
Expand Down Expand Up @@ -1075,6 +1103,41 @@ export const processEntityPermissions = (
}
}

const entitySubscriptions = entity.getSubscriptions();

if (!permissions.subscriptions && defaultPermissions) {
permissions.subscriptions = {};
}

if (permissions.subscriptions) {
passOrThrow(
isMap(permissions.subscriptions),
() =>
`Entity '${entity.name}' permissions definition for subscriptions needs to be a map of subscriptions and permissions`,
);

const subscriptionNames = Object.keys(permissions.subscriptions);
subscriptionNames.map((subscriptionName, idx) => {
passOrThrow(
isPermission(permissions.subscriptions[subscriptionName]) ||
isPermissionsArray(permissions.subscriptions[subscriptionName]),
() =>
`Invalid subscription permission definition for entity '${entity.name}' at position '${idx}'`,
);
});

if (defaultPermissions) {
entitySubscriptions.map(({ name: subscriptionName }) => {
if (defaultPermissions.subscriptions) {
permissions.subscriptions[subscriptionName] =
permissions.subscriptions[subscriptionName] ||
defaultPermissions.subscriptions[subscriptionName] ||
defaultPermissions.subscriptions._default;
}
});
}
}

if (permissions.find) {
validatePermissionAttributesAndStates(entity, permissions.find, 'find');
}
Expand Down Expand Up @@ -1107,6 +1170,36 @@ export const processEntityPermissions = (
});
}

if (permissions.subscriptions && entitySubscriptions) {
const permissionSubscriptionNames = Object.keys(permissions.subscriptions);

const subscriptionNames = entitySubscriptions.map(
subscription => subscription.name,
);

permissionSubscriptionNames.map(permissionSubscriptionName => {
passOrThrow(
subscriptionNames.includes(permissionSubscriptionName),
() =>
`Unknown subscription '${permissionSubscriptionName}' used for permissions in entity '${entity.name}'`,
);
});

entitySubscriptions.map(subscription => {
const subscriptionName = subscription.name;
const permission = permissions.subscriptions[subscriptionName];
if (permission) {
// not sure it's needed for subscription
validatePermissionSubscriptionTypes(entity, permission, subscription);
validatePermissionAttributesAndStates(
entity,
permission,
subscription.type,
);
}
});
}

const emptyPermissionsIn = findEmptyEntityPermissions(permissions);

passOrThrow(
Expand Down