Skip to content

Commit

Permalink
Merge pull request #158 from getlarge/subscription
Browse files Browse the repository at this point in the history
Subscription [WIP]
  • Loading branch information
chriskalmar committed May 12, 2020
2 parents 8154600 + 51cfdd2 commit c90e154
Show file tree
Hide file tree
Showing 17 changed files with 2,398 additions and 9 deletions.
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",
"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 = (
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

0 comments on commit c90e154

Please sign in to comment.