Merge pull request #548 from lowdefy/roles

Add role based authorisation.
This commit is contained in:
Gervwyk 2021-04-25 21:20:16 +02:00 committed by GitHub
commit 1edc7cb70f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1769 additions and 1090 deletions

View File

@ -20,13 +20,14 @@ import createFileLoader from './loaders/fileLoader';
import createFileSetter from './loaders/fileSetter';
import createMetaLoader from './loaders/metaLoader';
import buildConfig from './build/buildConfig';
import buildAuth from './build/buildAuth/buildAuth';
import buildConnections from './build/buildConnections';
import buildMenu from './build/buildMenu';
import buildPages from './build/buildPages';
import buildPages from './build/buildPages/buildPages';
import buildRefs from './build/buildRefs';
import cleanOutputDirectory from './build/cleanOutputDirectory';
import testSchema from './build/testSchema';
import validateConfig from './build/validateConfig';
import writeConfig from './build/writeConfig';
import writeConnections from './build/writeConnections';
import writeGlobal from './build/writeGlobal';
@ -53,7 +54,8 @@ async function build(options) {
let components = await buildRefs({ context });
await testSchema({ components, context });
context.metaLoader = createMetaLoader({ components, context });
await buildConfig({ components, context });
await validateConfig({ components, context });
await buildAuth({ components, context });
await buildConnections({ components, context });
await buildPages({ components, context });
await buildMenu({ components, context });

View File

@ -0,0 +1,58 @@
/* eslint-disable no-param-reassign */
/*
Copyright 2020-2021 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { type } from '@lowdefy/helpers';
import getPageRoles from './getPageRoles';
import getProtectedPages from './getProtectedPages';
function buildAuth({ components }) {
const protectedPages = getProtectedPages({ components });
const pageRoles = getPageRoles({ components });
let configPublicPages = [];
if (type.isArray(components.config.auth.pages.public)) {
configPublicPages = components.config.auth.pages.public;
}
(components.pages || []).forEach((page) => {
if (pageRoles[page.id]) {
if (configPublicPages.includes(page.id)) {
throw new Error(
`Page "${page.id}" is both protected by roles ${JSON.stringify(
pageRoles[page.id]
)} and public.`
);
}
page.auth = {
public: false,
roles: pageRoles[page.id],
};
} else if (protectedPages.includes(page.id)) {
page.auth = {
public: false,
};
} else {
page.auth = {
public: true,
};
}
});
return components;
}
export default buildAuth;

View File

@ -0,0 +1,325 @@
/*
Copyright 2020-2021 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import buildAuth from './buildAuth';
import validateConfig from '../validateConfig';
import testContext from '../../test/testContext';
const context = testContext();
test('buildAuth default', async () => {
const components = {
pages: [
{ id: 'a', type: 'Context' },
{ id: 'b', type: 'Context' },
{ id: 'c', type: 'Context' },
],
};
// validateConfig adds default values
validateConfig({ components });
const res = await buildAuth({ components, context });
expect(res).toEqual({
config: {
auth: {
pages: {
roles: {},
},
},
},
pages: [
{ id: 'a', type: 'Context', auth: { public: true } },
{ id: 'b', type: 'Context', auth: { public: true } },
{ id: 'c', type: 'Context', auth: { public: true } },
],
});
});
test('buildAuth no pages', async () => {
const components = {};
// validateConfig adds default values
validateConfig({ components });
const res = await buildAuth({ components, context });
expect(res).toEqual({
config: {
auth: {
pages: {
roles: {},
},
},
},
});
});
test('buildAuth all protected, some public', async () => {
const components = {
config: {
auth: {
pages: {
public: ['a', 'b'],
roles: {},
},
},
},
pages: [
{ id: 'a', type: 'Context' },
{ id: 'b', type: 'Context' },
{ id: 'c', type: 'Context' },
],
};
validateConfig({ components });
const res = await buildAuth({ components, context });
expect(res).toEqual({
config: {
auth: {
pages: {
public: ['a', 'b'],
roles: {},
},
},
},
pages: [
{ id: 'a', type: 'Context', auth: { public: true } },
{ id: 'b', type: 'Context', auth: { public: true } },
{ id: 'c', type: 'Context', auth: { public: false } },
],
});
});
test('buildAuth all public, some protected', async () => {
const components = {
config: {
auth: {
pages: {
protected: ['a', 'b'],
roles: {},
},
},
},
pages: [
{ id: 'a', type: 'Context' },
{ id: 'b', type: 'Context' },
{ id: 'c', type: 'Context' },
],
};
validateConfig({ components });
const res = await buildAuth({ components, context });
expect(res).toEqual({
config: {
auth: {
pages: {
protected: ['a', 'b'],
roles: {},
},
},
},
pages: [
{ id: 'a', type: 'Context', auth: { public: false } },
{ id: 'b', type: 'Context', auth: { public: false } },
{ id: 'c', type: 'Context', auth: { public: true } },
],
});
});
test('buildAuth all public', async () => {
const components = {
config: {
auth: {
pages: {
public: true,
roles: {},
},
},
},
pages: [
{ id: 'a', type: 'Context' },
{ id: 'b', type: 'Context' },
{ id: 'c', type: 'Context' },
],
};
validateConfig({ components });
const res = await buildAuth({ components, context });
expect(res).toEqual({
config: {
auth: {
pages: {
public: true,
roles: {},
},
},
},
pages: [
{ id: 'a', type: 'Context', auth: { public: true } },
{ id: 'b', type: 'Context', auth: { public: true } },
{ id: 'c', type: 'Context', auth: { public: true } },
],
});
});
test('buildAuth all protected', async () => {
const components = {
config: {
auth: {
pages: {
protected: true,
roles: {},
},
},
},
pages: [
{ id: 'a', type: 'Context' },
{ id: 'b', type: 'Context' },
{ id: 'c', type: 'Context' },
],
};
validateConfig({ components });
const res = await buildAuth({ components, context });
expect(res).toEqual({
config: {
auth: {
pages: {
protected: true,
roles: {},
},
},
},
pages: [
{ id: 'a', type: 'Context', auth: { public: false } },
{ id: 'b', type: 'Context', auth: { public: false } },
{ id: 'c', type: 'Context', auth: { public: false } },
],
});
});
test('buildAuth roles', async () => {
const components = {
config: {
auth: {
pages: {
roles: {
role1: ['page1'],
role2: ['page1', 'page2'],
},
},
},
},
pages: [
{ id: 'page1', type: 'Context' },
{ id: 'page2', type: 'Context' },
{ id: 'page3', type: 'Context' },
],
};
validateConfig({ components });
const res = await buildAuth({ components, context });
expect(res).toEqual({
config: {
auth: {
pages: {
roles: {
role1: ['page1'],
role2: ['page1', 'page2'],
},
},
},
},
pages: [
{ id: 'page1', type: 'Context', auth: { public: false, roles: ['role1', 'role2'] } },
{ id: 'page2', type: 'Context', auth: { public: false, roles: ['role2'] } },
{ id: 'page3', type: 'Context', auth: { public: true } },
],
});
});
test('buildAuth roles and public pages inconsistency', async () => {
const components = {
config: {
auth: {
pages: {
roles: {
role1: ['page1'],
},
public: ['page1'],
},
},
},
pages: [{ id: 'page1', type: 'Context' }],
};
validateConfig({ components });
expect(() => buildAuth({ components, context })).toThrow(
'Page "page1" is both protected by roles ["role1"] and public.'
);
});
test('buildAuth roles and protected pages array', async () => {
const components = {
config: {
auth: {
pages: {
roles: {
role1: ['page1'],
},
protected: ['page1'],
},
},
},
pages: [{ id: 'page1', type: 'Context' }],
};
validateConfig({ components });
const res = await buildAuth({ components, context });
expect(res).toEqual({
config: {
auth: {
pages: {
roles: {
role1: ['page1'],
},
protected: ['page1'],
},
},
},
pages: [{ id: 'page1', type: 'Context', auth: { public: false, roles: ['role1'] } }],
});
});
test('buildAuth roles and protected true', async () => {
const components = {
config: {
auth: {
pages: {
roles: {
role1: ['page1'],
},
protected: true,
},
},
},
pages: [{ id: 'page1', type: 'Context' }],
};
validateConfig({ components });
const res = await buildAuth({ components, context });
expect(res).toEqual({
config: {
auth: {
pages: {
roles: {
role1: ['page1'],
},
protected: true,
},
},
},
pages: [{ id: 'page1', type: 'Context', auth: { public: false, roles: ['role1'] } }],
});
});

View File

@ -0,0 +1,34 @@
/*
Copyright 2020-2021 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
function getPageRoles({ components }) {
const roles = components.config.auth.pages.roles;
const pageRoles = {};
Object.keys(roles).forEach((roleName) => {
roles[roleName].forEach((pageId) => {
if (!pageRoles[pageId]) {
pageRoles[pageId] = new Set();
}
pageRoles[pageId].add(roleName);
});
});
Object.keys(pageRoles).forEach((pageId) => {
pageRoles[pageId] = [...pageRoles[pageId]];
});
return pageRoles;
}
export default getPageRoles;

View File

@ -0,0 +1,72 @@
/*
Copyright 2020-2021 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import getPageRoles from './getPageRoles';
test('No roles', () => {
const components = {
config: {
auth: {
pages: {
roles: {},
},
},
},
};
const res = getPageRoles({ components });
expect(res).toEqual({});
});
test('Roles, 1 page per role', () => {
const components = {
config: {
auth: {
pages: {
roles: {
role1: ['page1'],
role2: ['page2'],
},
},
},
},
};
const res = getPageRoles({ components });
expect(res).toEqual({
page1: ['role1'],
page2: ['role2'],
});
});
test('Multiple roles on a page', () => {
const components = {
config: {
auth: {
pages: {
roles: {
role1: ['page1', 'page2'],
role2: ['page2', 'page3'],
},
},
},
},
};
const res = getPageRoles({ components });
expect(res).toEqual({
page1: ['role1'],
page2: ['role1', 'role2'],
page3: ['role2'],
});
});

View File

@ -0,0 +1,35 @@
/*
Copyright 2020-2021 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { type } from '@lowdefy/helpers';
function getProtectedPages({ components }) {
const pageIds = (components.pages || []).map((page) => page.id);
let protectedPages = [];
if (type.isArray(components.config.auth.pages.public)) {
protectedPages = pageIds.filter(
(pageId) => !components.config.auth.pages.public.includes(pageId)
);
} else if (components.config.auth.pages.protected === true) {
protectedPages = pageIds;
} else if (type.isArray(components.config.auth.pages.protected)) {
protectedPages = components.config.auth.pages.protected;
}
return protectedPages;
}
export default getProtectedPages;

View File

@ -0,0 +1,223 @@
/*
Copyright 2020-2021 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import getProtectedPages from './getProtectedPages';
test('No config', () => {
const components = {
config: {
auth: {
pages: {},
},
},
};
const res = getProtectedPages({ components });
expect(res).toEqual([]);
});
test('Public true', () => {
const components = {
config: {
auth: {
pages: {
public: true,
},
},
},
pages: [
{ id: 'a', type: 'Context' },
{ id: 'b', type: 'Context' },
{ id: 'c', type: 'Context' },
],
};
const res = getProtectedPages({ components });
expect(res).toEqual([]);
});
test('Protected empty array', () => {
const components = {
config: {
auth: {
pages: {
protected: [],
},
},
},
pages: [
{ id: 'a', type: 'Context' },
{ id: 'b', type: 'Context' },
{ id: 'c', type: 'Context' },
],
};
const res = getProtectedPages({ components });
expect(res).toEqual([]);
});
test('Protected empty array, public true', () => {
const components = {
config: {
auth: {
pages: {
protected: [],
public: true,
},
},
},
pages: [
{ id: 'a', type: 'Context' },
{ id: 'b', type: 'Context' },
{ id: 'c', type: 'Context' },
],
};
const res = getProtectedPages({ components });
expect(res).toEqual([]);
});
test('Protected true', () => {
const components = {
config: {
auth: {
pages: {
protected: true,
},
},
},
pages: [
{ id: 'a', type: 'Context' },
{ id: 'b', type: 'Context' },
{ id: 'c', type: 'Context' },
],
};
const res = getProtectedPages({ components });
expect(res).toEqual(['a', 'b', 'c']);
});
test('Public empty array', () => {
const components = {
config: {
auth: {
pages: {
public: [],
},
},
},
pages: [
{ id: 'a', type: 'Context' },
{ id: 'b', type: 'Context' },
{ id: 'c', type: 'Context' },
],
};
const res = getProtectedPages({ components });
expect(res).toEqual(['a', 'b', 'c']);
});
test('Protected true, public empty array', () => {
const components = {
config: {
auth: {
pages: {
protected: true,
public: [],
},
},
},
pages: [
{ id: 'a', type: 'Context' },
{ id: 'b', type: 'Context' },
{ id: 'c', type: 'Context' },
],
};
const res = getProtectedPages({ components });
expect(res).toEqual(['a', 'b', 'c']);
});
test('Protected true, public array', () => {
const components = {
config: {
auth: {
pages: {
protected: true,
public: ['a'],
},
},
},
pages: [
{ id: 'a', type: 'Context' },
{ id: 'b', type: 'Context' },
{ id: 'c', type: 'Context' },
],
};
const res = getProtectedPages({ components });
expect(res).toEqual(['b', 'c']);
});
test('Public array', () => {
const components = {
config: {
auth: {
pages: {
public: ['a'],
},
},
},
pages: [
{ id: 'a', type: 'Context' },
{ id: 'b', type: 'Context' },
{ id: 'c', type: 'Context' },
],
};
const res = getProtectedPages({ components });
expect(res).toEqual(['b', 'c']);
});
test('Protected array', () => {
const components = {
config: {
auth: {
pages: {
protected: ['a'],
},
},
},
pages: [
{ id: 'a', type: 'Context' },
{ id: 'b', type: 'Context' },
{ id: 'c', type: 'Context' },
],
};
const res = getProtectedPages({ components });
expect(res).toEqual(['a']);
});
test('Protected array, public true', () => {
const components = {
config: {
auth: {
pages: {
protected: ['a'],
public: true,
},
},
},
pages: [
{ id: 'a', type: 'Context' },
{ id: 'b', type: 'Context' },
{ id: 'c', type: 'Context' },
],
};
const res = getProtectedPages({ components });
expect(res).toEqual(['a']);
});

View File

@ -1,211 +0,0 @@
/*
Copyright 2020-2021 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import buildConfig from './buildConfig';
import testContext from '../test/testContext';
const context = testContext();
test('buildConfig config not an object', async () => {
const components = {
config: 'config',
};
await expect(buildConfig({ components, context })).rejects.toThrow('Config is not an object.');
});
test('buildConfig config error when both protected and public pages are both arrays', async () => {
const components = {
config: {
auth: {
pages: {
protected: [],
public: [],
},
},
},
};
await expect(buildConfig({ components, context })).rejects.toThrow(
'Protected and public pages are mutually exclusive. When protected pages are listed, all unlisted pages are public by default and visa versa.'
);
});
test('buildConfig config error when both protected and public pages are true', async () => {
const components = {
config: {
auth: {
pages: {
protected: true,
public: true,
},
},
},
};
await expect(buildConfig({ components, context })).rejects.toThrow(
'Protected and public pages are mutually exclusive. When protected pages are listed, all unlisted pages are public by default and visa versa.'
);
});
test('buildConfig config error when both protected or public are false.', async () => {
let components = {
config: {
auth: {
pages: {
protected: false,
},
},
},
};
await expect(buildConfig({ components, context })).rejects.toThrow(
'Protected pages can not be set to false.'
);
components = {
config: {
auth: {
pages: {
public: false,
},
},
},
};
await expect(buildConfig({ components, context })).rejects.toThrow(
'Public pages can not be set to false.'
);
});
test('buildConfig default', async () => {
const components = {};
const res = await buildConfig({ components, context });
expect(res).toEqual({
config: {
auth: {
pages: {},
},
},
auth: {
include: [],
set: 'public',
default: 'public',
},
});
});
test('buildConfig all protected, some public', async () => {
const components = {
config: {
auth: {
pages: {
public: ['a', 'b'],
},
},
},
};
const res = await buildConfig({ components, context });
expect(res).toEqual({
config: {
auth: {
pages: {
public: ['a', 'b'],
},
},
},
auth: {
include: ['a', 'b'],
set: 'public',
default: 'protected',
},
});
});
test('buildConfig all public, some protected', async () => {
const components = {
config: {
auth: {
pages: {
protected: ['a', 'b'],
},
},
},
};
const res = await buildConfig({ components, context });
expect(res).toEqual({
config: {
auth: {
pages: {
protected: ['a', 'b'],
},
},
},
auth: {
include: ['a', 'b'],
set: 'protected',
default: 'public',
},
});
});
test('buildConfig all public', async () => {
const components = {
config: {
auth: {
pages: {
public: true,
},
},
},
};
const res = await buildConfig({ components, context });
expect(res).toEqual({
config: {
auth: {
pages: {
public: true,
},
},
},
auth: {
include: [],
set: 'protected',
default: 'public',
},
});
});
test('buildConfig all protected', async () => {
const components = {
config: {
auth: {
pages: {
protected: true,
},
},
},
};
const res = await buildConfig({ components, context });
expect(res).toEqual({
config: {
auth: {
pages: {
protected: true,
},
},
},
auth: {
include: [],
set: 'public',
default: 'protected',
},
});
});

View File

@ -54,11 +54,11 @@ function loopItems(parent, menuId, pages, missingPageWarnings) {
menuItem.auth = page.auth;
}
} else {
menuItem.auth = 'public';
menuItem.auth = { public: true };
}
}
if (menuItem.type === 'MenuGroup') {
menuItem.auth = 'public';
menuItem.auth = { public: true };
}
menuItem.menuItemId = menuItem.id;
menuItem.id = `menuitem:${menuId}:${menuItem.id}`;

View File

@ -66,12 +66,12 @@ test('buildMenu menus exist', async () => {
{
id: 'page:page_1',
pageId: 'page_1',
auth: 'public',
auth: { public: true },
},
{
id: 'page:page_2',
pageId: 'page_2',
auth: 'protected',
auth: { public: false },
},
],
};
@ -90,7 +90,7 @@ test('buildMenu menus exist', async () => {
},
type: 'MenuLink',
pageId: 'page_1',
auth: 'public',
auth: { public: true },
},
{
id: 'menuitem:my_menu:menu_page_2',
@ -100,7 +100,7 @@ test('buildMenu menus exist', async () => {
},
type: 'MenuLink',
pageId: 'page_2',
auth: 'protected',
auth: { public: false },
},
{
id: 'menuitem:my_menu:menu_external',
@ -110,7 +110,7 @@ test('buildMenu menus exist', async () => {
},
type: 'MenuLink',
url: 'www.lowdefy.com',
auth: 'public',
auth: { public: true },
},
],
},
@ -119,12 +119,12 @@ test('buildMenu menus exist', async () => {
{
id: 'page:page_1',
pageId: 'page_1',
auth: 'public',
auth: { public: true },
},
{
id: 'page:page_2',
pageId: 'page_2',
auth: 'protected',
auth: { public: false },
},
],
});
@ -157,7 +157,7 @@ test('buildMenu nested menus', async () => {
{
id: 'page:page_1',
pageId: 'page_1',
auth: 'public',
auth: { public: true },
},
],
};
@ -172,7 +172,7 @@ test('buildMenu nested menus', async () => {
id: 'menuitem:my_menu:group',
menuItemId: 'group',
type: 'MenuGroup',
auth: 'public',
auth: { public: true },
links: [
{
id: 'menuitem:my_menu:menu_page_1',
@ -182,7 +182,7 @@ test('buildMenu nested menus', async () => {
},
type: 'MenuLink',
pageId: 'page_1',
auth: 'public',
auth: { public: true },
},
],
},
@ -193,7 +193,7 @@ test('buildMenu nested menus', async () => {
{
id: 'page:page_1',
pageId: 'page_1',
auth: 'public',
auth: { public: true },
},
],
});
@ -205,17 +205,17 @@ test('buildMenu default menu', async () => {
{
id: 'page:page_1',
pageId: 'page_1',
auth: 'public',
auth: { public: true },
},
{
id: 'page:page_2',
pageId: 'page_2',
auth: 'public',
auth: { public: true },
},
{
id: 'page:page_3',
pageId: 'page_3',
auth: 'public',
auth: { public: true },
},
],
};
@ -231,21 +231,21 @@ test('buildMenu default menu', async () => {
menuItemId: '0',
type: 'MenuLink',
pageId: 'page_1',
auth: 'public',
auth: { public: true },
},
{
id: 'menuitem:default:1',
menuItemId: '1',
type: 'MenuLink',
pageId: 'page_2',
auth: 'public',
auth: { public: true },
},
{
id: 'menuitem:default:2',
menuItemId: '2',
type: 'MenuLink',
pageId: 'page_3',
auth: 'public',
auth: { public: true },
},
],
},
@ -254,17 +254,17 @@ test('buildMenu default menu', async () => {
{
id: 'page:page_1',
pageId: 'page_1',
auth: 'public',
auth: { public: true },
},
{
id: 'page:page_2',
pageId: 'page_2',
auth: 'public',
auth: { public: true },
},
{
id: 'page:page_3',
pageId: 'page_3',
auth: 'public',
auth: { public: true },
},
],
});
@ -366,20 +366,20 @@ test('buildMenu page does not exist, nested', async () => {
id: 'menuitem:my_menu:MenuGroup1',
menuItemId: 'MenuGroup1',
type: 'MenuGroup',
auth: 'public',
auth: { public: true },
links: [],
},
{
id: 'menuitem:my_menu:MenuGroup2',
menuItemId: 'MenuGroup2',
type: 'MenuGroup',
auth: 'public',
auth: { public: true },
links: [
{
id: 'menuitem:my_menu:MenuGroup3',
menuItemId: 'MenuGroup3',
type: 'MenuGroup',
auth: 'public',
auth: { public: true },
links: [],
},
],
@ -429,7 +429,7 @@ test('buildMenu pages not array, menu exists', async () => {
},
type: 'MenuLink',
url: 'www.lowdefy.com',
auth: 'public',
auth: { public: true },
},
],
},

View File

@ -1,235 +0,0 @@
/* eslint-disable no-param-reassign */
/*
Copyright 2020-2021 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { get, set, type } from '@lowdefy/helpers';
/* Page and block build steps
Pages:
- set pageId = id
- set id = `page:${page.id}`
Blocks:
- set blockId = id
- set id = `block:${pageId}:${block.id}` if not a page
- set request ids
- set block meta
- set blocks to areas.content
- set operators list on context blocks
*/
function getContextOperators(block) {
const stripContext = (_, value) => {
if (get(value, 'meta.category') === 'context') {
return null;
}
return value;
};
const { requests, ...webBlock } = block;
webBlock.areas = JSON.parse(JSON.stringify(webBlock.areas || {}), stripContext);
const operators = new Set();
const pushOperators = (_, value) => {
if (type.isObject(value) && Object.keys(value).length === 1) {
const key = Object.keys(value)[0];
const [op, _] = key.split('.');
const operator = op.replace(/^(\_+)/gm, '_');
if (operator.length > 1 && operator[0] === '_') {
operators.add(operator);
}
}
return value;
};
JSON.parse(JSON.stringify(webBlock), pushOperators);
return [...operators];
}
function fillContextOperators(block) {
if (get(block, 'meta.category') === 'context') {
block.operators = getContextOperators(block);
}
Object.keys(block.areas || {}).forEach((key) => {
block.areas[key].blocks.map((blk) => {
fillContextOperators(blk);
});
});
}
function buildRequests(block, context) {
if (!type.isNone(block.requests)) {
if (!type.isArray(block.requests)) {
throw new Error(
`Requests is not an array at ${block.blockId} on page ${
context.pageId
}. Received ${JSON.stringify(block.requests)}`
);
}
block.requests.forEach((request) => {
request.auth = context.auth;
request.requestId = request.id;
request.contextId = context.contextId;
request.id = `request:${context.pageId}:${context.contextId}:${request.id}`;
context.requests.push(request);
});
delete block.requests;
}
}
async function checkPageIsContext(page, metaLoader) {
if (type.isNone(page.type)) {
throw new Error(`Page type is not defined at ${page.pageId}.`);
}
if (!type.isString(page.type)) {
throw new Error(
`Page type is not a string at ${page.pageId}. Received ${JSON.stringify(page.type)}`
);
}
const meta = await metaLoader.load(page.type);
if (!meta) {
throw new Error(
`Invalid block type at page ${page.pageId}. Received ${JSON.stringify(page.type)}`
);
}
if (meta.category !== 'context') {
throw new Error(
`Page ${page.pageId} is not of category "context". Received ${JSON.stringify(page.type)}`
);
}
}
async function setBlockMeta(block, metaLoader, pageId) {
if (type.isNone(block.type)) {
throw new Error(`Block type is not defined at ${block.blockId} on page ${pageId}.`);
}
if (!type.isString(block.type)) {
throw new Error(
`Block type is not a string at ${block.blockId} on page ${pageId}. Received ${JSON.stringify(
block.type
)}`
);
}
const meta = await metaLoader.load(block.type);
if (!meta) {
throw new Error(
`Invalid Block type at ${block.blockId} on page ${pageId}. Received ${JSON.stringify(
block.type
)}`
);
}
const { category, loading, moduleFederation, valueType } = meta;
block.meta = { category, loading, moduleFederation };
if (category === 'input') {
block.meta.valueType = valueType;
}
if (category === 'list') {
// include valueType to ensure block has value on init
block.meta.valueType = 'array';
}
// Add user defined loading
if (block.loading) {
block.meta.loading = block.loading;
}
}
async function buildBlock(block, context) {
if (!type.isObject(block)) {
throw new Error(
`Expected block to be an object on ${context.pageId}. Received ${JSON.stringify(block)}`
);
}
if (type.isUndefined(block.id)) {
throw new Error(`Block id missing at page ${context.pageId}`);
}
block.blockId = block.id;
block.id = `block:${context.pageId}:${block.id}`;
await setBlockMeta(block, context.metaLoader, context.pageId);
let ctx = context;
if (block.meta.category === 'context') {
ctx = {
auth: context.auth,
contextId: block.blockId,
metaLoader: context.metaLoader,
pageId: context.pageId,
requests: [],
};
}
buildRequests(block, ctx);
if (block.meta.category === 'context') {
block.requests = ctx.requests;
}
if (!type.isNone(block.blocks)) {
if (!type.isArray(block.blocks)) {
throw new Error(
`Blocks at ${block.blockId} on page ${
ctx.pageId
} is not an array. Received ${JSON.stringify(block.blocks)}`
);
}
set(block, 'areas.content.blocks', block.blocks);
delete block.blocks;
}
if (type.isObject(block.areas)) {
let promises = [];
Object.keys(block.areas).forEach((key) => {
if (type.isNone(block.areas[key].blocks)) {
block.areas[key].blocks = [];
}
if (!type.isArray(block.areas[key].blocks)) {
throw new Error(
`Expected blocks to be an array at ${block.blockId} in area ${key} on page ${
ctx.pageId
}. Received ${JSON.stringify(block.areas[key].blocks)}`
);
}
const blockPromises = block.areas[key].blocks.map(async (blk) => {
await buildBlock(blk, ctx);
});
promises = promises.concat(blockPromises);
});
await Promise.all(promises);
}
}
async function buildPages({ components, context }) {
const pages = type.isArray(components.pages) ? components.pages : [];
const pageBuildPromises = pages.map(async (page, i) => {
if (type.isUndefined(page.id)) {
throw new Error(`Page id missing at page ${i}`);
}
page.pageId = page.id;
await checkPageIsContext(page, context.metaLoader);
if (components.auth.include.includes(page.pageId)) {
page.auth = components.auth.set;
} else {
page.auth = components.auth.default;
}
await buildBlock(page, {
auth: page.auth,
pageId: page.pageId,
requests: [],
metaLoader: context.metaLoader,
});
// set page.id since buildBlock sets id as well.
page.id = `page:${page.pageId}`;
fillContextOperators(page);
});
await Promise.all(pageBuildPromises);
return components;
}
export default buildPages;

View File

@ -0,0 +1,80 @@
/*
Copyright 2020-2021 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { set, type } from '@lowdefy/helpers';
import buildRequests from './buildRequests';
import setBlockMeta from './setBlockMeta';
async function buildBlock(block, blockContext) {
if (!type.isObject(block)) {
throw new Error(
`Expected block to be an object on ${blockContext.pageId}. Received ${JSON.stringify(block)}`
);
}
if (type.isUndefined(block.id)) {
throw new Error(`Block id missing at page ${blockContext.pageId}`);
}
block.blockId = block.id;
block.id = `block:${blockContext.pageId}:${block.id}`;
await setBlockMeta(block, blockContext.metaLoader, blockContext.pageId);
let newBlockContext = blockContext;
if (block.meta.category === 'context') {
newBlockContext = {
auth: blockContext.auth,
contextId: block.blockId,
metaLoader: blockContext.metaLoader,
pageId: blockContext.pageId,
requests: [],
};
}
buildRequests(block, newBlockContext);
if (block.meta.category === 'context') {
block.requests = newBlockContext.requests;
}
if (!type.isNone(block.blocks)) {
if (!type.isArray(block.blocks)) {
throw new Error(
`Blocks at ${block.blockId} on page ${
newBlockContext.pageId
} is not an array. Received ${JSON.stringify(block.blocks)}`
);
}
set(block, 'areas.content.blocks', block.blocks);
delete block.blocks;
}
if (type.isObject(block.areas)) {
let promises = [];
Object.keys(block.areas).forEach((key) => {
if (type.isNone(block.areas[key].blocks)) {
block.areas[key].blocks = [];
}
if (!type.isArray(block.areas[key].blocks)) {
throw new Error(
`Expected blocks to be an array at ${block.blockId} in area ${key} on page ${
newBlockContext.pageId
}. Received ${JSON.stringify(block.areas[key].blocks)}`
);
}
const blockPromises = block.areas[key].blocks.map(async (blk) => {
await buildBlock(blk, newBlockContext);
});
promises = promises.concat(blockPromises);
});
await Promise.all(promises);
}
}
export default buildBlock;

View File

@ -0,0 +1,61 @@
/* eslint-disable no-param-reassign */
/*
Copyright 2020-2021 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { type } from '@lowdefy/helpers';
import buildBlock from './buildBlock';
import checkPageIsContext from './checkPageIsContext';
import fillContextOperators from './fillContextOperators';
/* Page and block build steps
Pages:
- set pageId = id
- set id = `page:${page.id}`
Blocks:
- set blockId = id
- set id = `block:${pageId}:${block.id}` if not a page
- set request ids
- set block meta
- set blocks to areas.content
- set operators list on context blocks
*/
async function buildPages({ components, context }) {
const pages = type.isArray(components.pages) ? components.pages : [];
const pageBuildPromises = pages.map(async (page, i) => {
if (type.isUndefined(page.id)) {
throw new Error(`Page id missing at page ${i}`);
}
page.pageId = page.id;
await checkPageIsContext(page, context.metaLoader);
await buildBlock(page, {
auth: page.auth,
pageId: page.pageId,
requests: [],
metaLoader: context.metaLoader,
});
// set page.id since buildBlock sets id as well.
page.id = `page:${page.pageId}`;
fillContextOperators(page);
});
await Promise.all(pageBuildPromises);
return components;
}
export default buildPages;

View File

@ -15,7 +15,7 @@
*/
import { get } from '@lowdefy/helpers';
import buildPages from './buildPages';
import testContext from '../test/testContext';
import testContext from '../../test/testContext';
const mockLogWarn = jest.fn();
const mockLog = jest.fn();
@ -165,9 +165,7 @@ const outputMetas = {
};
const auth = {
default: 'public',
include: [],
set: 'public',
public: true,
};
const mockMetaLoader = (type) => {
@ -197,22 +195,20 @@ test('buildPages no pages', async () => {
test('buildPages pages not an array', async () => {
const components = {
auth,
pages: 'pages',
};
const res = await buildPages({ components, context });
expect(res).toEqual({
auth,
pages: 'pages',
});
});
test('page does not have an id', async () => {
const components = {
auth,
pages: [
{
type: 'Context',
auth,
},
],
};
@ -221,11 +217,11 @@ test('page does not have an id', async () => {
test('block does not have an id', async () => {
const components = {
auth,
pages: [
{
id: 'page1',
type: 'Context',
auth,
blocks: [
{
type: 'Input',
@ -241,10 +237,10 @@ test('block does not have an id', async () => {
test('page type missing', async () => {
const components = {
auth,
pages: [
{
id: 'page1',
auth,
},
],
};
@ -255,11 +251,11 @@ test('page type missing', async () => {
test('block type missing', async () => {
const components = {
auth,
pages: [
{
id: 'page1',
type: 'Context',
auth,
blocks: [
{
id: 'blockId',
@ -275,11 +271,11 @@ test('block type missing', async () => {
test('invalid page type', async () => {
const components = {
auth,
pages: [
{
id: 'page1',
type: 'NotABlock',
auth,
},
],
};
@ -290,11 +286,11 @@ test('invalid page type', async () => {
test('invalid block type', async () => {
const components = {
auth,
pages: [
{
id: 'page1',
type: 'Context',
auth,
blocks: [
{
id: 'blockId',
@ -311,11 +307,11 @@ test('invalid block type', async () => {
test('page type not a string', async () => {
const components = {
auth,
pages: [
{
id: 'page1',
type: 1,
auth,
},
],
};
@ -326,11 +322,11 @@ test('page type not a string', async () => {
test('block type not a string', async () => {
const components = {
auth,
pages: [
{
id: 'page1',
type: 'Context',
auth,
blocks: [
{
id: 'blockId',
@ -347,11 +343,11 @@ test('block type not a string', async () => {
test('page type is not of category context', async () => {
const components = {
auth,
pages: [
{
id: 'page1',
type: 'Container',
auth,
},
],
};
@ -362,21 +358,20 @@ test('page type is not of category context', async () => {
test('no blocks on page', async () => {
const components = {
auth,
pages: [
{
id: '1',
type: 'Context',
auth,
},
],
};
const res = await buildPages({ components, context });
expect(res).toEqual({
auth,
pages: [
{
id: 'page:1',
auth: 'public',
auth: { public: true },
operators: [],
pageId: '1',
blockId: '1',
@ -390,7 +385,6 @@ test('no blocks on page', async () => {
test('blocks not an array', async () => {
const components = {
auth,
pages: [
{
id: 'page1',
@ -406,7 +400,6 @@ test('blocks not an array', async () => {
test('block not an object', async () => {
const components = {
auth,
pages: [
{
id: 'page1',
@ -422,11 +415,11 @@ test('block not an object', async () => {
test('block meta should include all meta fields', async () => {
const components = {
auth,
pages: [
{
id: 'page_1',
type: 'Context',
auth,
blocks: [
{
id: 'block_1',
@ -446,11 +439,10 @@ test('block meta should include all meta fields', async () => {
};
const res = await buildPages({ components, context });
expect(res).toEqual({
auth,
pages: [
{
id: 'page:page_1',
auth: 'public',
auth: { public: true },
operators: [],
pageId: 'page_1',
blockId: 'page_1',
@ -488,11 +480,11 @@ test('block meta should include all meta fields', async () => {
test('nested blocks', async () => {
const components = {
auth,
pages: [
{
id: 'page_1',
type: 'Context',
auth,
blocks: [
{
id: 'block_1',
@ -510,11 +502,10 @@ test('nested blocks', async () => {
};
const res = await buildPages({ components, context });
expect(res).toEqual({
auth,
pages: [
{
id: 'page:page_1',
auth: 'public',
auth: { public: true },
operators: [],
pageId: 'page_1',
blockId: 'page_1',
@ -553,11 +544,11 @@ test('nested blocks', async () => {
describe('block areas', () => {
test('content area blocks is not an array', async () => {
const components = {
auth,
pages: [
{
id: 'page1',
type: 'Context',
auth,
areas: {
content: {
blocks: 'string',
@ -573,11 +564,11 @@ describe('block areas', () => {
test('Add array if area blocks is undefined', async () => {
const components = {
auth,
pages: [
{
id: 'page1',
type: 'Context',
auth,
areas: {
content: {},
},
@ -586,11 +577,10 @@ describe('block areas', () => {
};
const res = await buildPages({ components, context });
expect(res).toEqual({
auth,
pages: [
{
id: 'page:page1',
auth: 'public',
auth: { public: true },
blockId: 'page1',
operators: [],
pageId: 'page1',
@ -609,11 +599,11 @@ describe('block areas', () => {
test('content area on page ', async () => {
const components = {
auth,
pages: [
{
id: '1',
type: 'Context',
auth,
areas: {
content: {
blocks: [
@ -629,11 +619,10 @@ describe('block areas', () => {
};
const res = await buildPages({ components, context });
expect(res).toEqual({
auth,
pages: [
{
id: 'page:1',
auth: 'public',
auth: { public: true },
blockId: '1',
operators: [],
pageId: '1',
@ -659,12 +648,11 @@ describe('block areas', () => {
test('does not overwrite area layout', async () => {
const components = {
auth,
pages: [
{
id: '1',
auth: 'public',
type: 'Context',
auth,
areas: {
content: {
gutter: 20,
@ -681,11 +669,10 @@ describe('block areas', () => {
};
const res = await buildPages({ components, context });
expect(res).toEqual({
auth,
pages: [
{
id: 'page:1',
auth: 'public',
auth: { public: true },
pageId: '1',
operators: [],
blockId: '1',
@ -712,11 +699,11 @@ describe('block areas', () => {
test('multiple content areas on page ', async () => {
const components = {
auth,
pages: [
{
id: '1',
type: 'Context',
auth,
areas: {
content: {
blocks: [
@ -740,11 +727,10 @@ describe('block areas', () => {
};
const res = await buildPages({ components, context });
expect(res).toEqual({
auth,
pages: [
{
id: 'page:1',
auth: 'public',
auth: { public: true },
operators: [],
pageId: '1',
blockId: '1',
@ -780,11 +766,11 @@ describe('block areas', () => {
test('blocks array does not affect other content areas', async () => {
const components = {
auth,
pages: [
{
id: '1',
type: 'Context',
auth,
blocks: [
{
id: 'textInput',
@ -806,11 +792,10 @@ describe('block areas', () => {
};
const res = await buildPages({ components, context });
expect(res).toEqual({
auth,
pages: [
{
id: 'page:1',
auth: 'public',
auth: { public: true },
operators: [],
pageId: '1',
blockId: '1',
@ -846,11 +831,11 @@ describe('block areas', () => {
test('blocks array overwrites areas.content', async () => {
const components = {
auth,
pages: [
{
id: '1',
type: 'Context',
auth,
blocks: [
{
id: 'textInput',
@ -880,11 +865,10 @@ describe('block areas', () => {
};
const res = await buildPages({ components, context });
expect(res).toEqual({
auth,
pages: [
{
id: 'page:1',
auth: 'public',
auth: { public: true },
operators: [],
pageId: '1',
blockId: '1',
@ -920,11 +904,11 @@ describe('block areas', () => {
test('nested content areas ', async () => {
const components = {
auth,
pages: [
{
id: '1',
type: 'Context',
auth,
blocks: [
{
id: 'card',
@ -970,11 +954,10 @@ describe('block areas', () => {
};
const res = await buildPages({ components, context });
expect(res).toEqual({
auth,
pages: [
{
id: 'page:1',
auth: 'public',
auth: { public: true },
operators: [],
pageId: '1',
blockId: '1',
@ -1046,10 +1029,10 @@ describe('block areas', () => {
describe('build requests', () => {
test('requests not an array', async () => {
const components = {
auth,
pages: [
{
id: 'page_1',
auth,
type: 'Context',
requests: 'requests',
},
@ -1062,11 +1045,11 @@ describe('build requests', () => {
test('give request an id', async () => {
const components = {
auth,
pages: [
{
id: 'page_1',
type: 'Context',
auth,
requests: [
{
id: 'request_1',
@ -1077,11 +1060,10 @@ describe('build requests', () => {
};
const res = await buildPages({ components, context });
expect(res).toEqual({
auth,
pages: [
{
id: 'page:page_1',
auth: 'public',
auth: { public: true },
operators: [],
pageId: 'page_1',
blockId: 'page_1',
@ -1090,7 +1072,7 @@ describe('build requests', () => {
requests: [
{
id: 'request:page_1:page_1:request_1',
auth: 'public',
auth: { public: true },
requestId: 'request_1',
contextId: 'page_1',
},
@ -1102,11 +1084,11 @@ describe('build requests', () => {
test('request on a context block not at root', async () => {
const components = {
auth,
pages: [
{
id: 'page_1',
type: 'Context',
auth,
blocks: [
{
id: 'context',
@ -1123,11 +1105,10 @@ describe('build requests', () => {
};
const res = await buildPages({ components, context });
expect(res).toEqual({
auth,
pages: [
{
id: 'page:page_1',
auth: 'public',
auth: { public: true },
operators: [],
pageId: 'page_1',
blockId: 'page_1',
@ -1146,7 +1127,7 @@ describe('build requests', () => {
requests: [
{
id: 'request:page_1:context:request_1',
auth: 'public',
auth: { public: true },
requestId: 'request_1',
contextId: 'context',
},
@ -1162,11 +1143,11 @@ describe('build requests', () => {
test('request on a non-context block', async () => {
const components = {
auth,
pages: [
{
id: 'page_1',
type: 'Context',
auth,
blocks: [
{
id: 'box',
@ -1183,11 +1164,10 @@ describe('build requests', () => {
};
const res = await buildPages({ components, context });
expect(res).toEqual({
auth,
pages: [
{
id: 'page:page_1',
auth: 'public',
auth: { public: true },
blockId: 'page_1',
operators: [],
pageId: 'page_1',
@ -1196,7 +1176,7 @@ describe('build requests', () => {
requests: [
{
id: 'request:page_1:page_1:request_1',
auth: 'public',
auth: { public: true },
requestId: 'request_1',
contextId: 'page_1',
},
@ -1220,11 +1200,11 @@ describe('build requests', () => {
test('request on a non-context block below a context block not at root', async () => {
const components = {
auth,
pages: [
{
id: 'page_1',
type: 'Context',
auth,
blocks: [
{
id: 'context',
@ -1247,11 +1227,10 @@ describe('build requests', () => {
};
const res = await buildPages({ components, context });
expect(res).toEqual({
auth,
pages: [
{
id: 'page:page_1',
auth: 'public',
auth: { public: true },
operators: [],
pageId: 'page_1',
blockId: 'page_1',
@ -1270,7 +1249,7 @@ describe('build requests', () => {
requests: [
{
id: 'request:page_1:context:request_1',
auth: 'public',
auth: { public: true },
requestId: 'request_1',
contextId: 'context',
},
@ -1298,11 +1277,11 @@ describe('build requests', () => {
test('request on a non-context block below a context block and at root', async () => {
const components = {
auth,
pages: [
{
id: 'page_1',
type: 'Context',
auth,
blocks: [
{
id: 'context',
@ -1329,11 +1308,10 @@ describe('build requests', () => {
};
const res = await buildPages({ components, context });
expect(res).toEqual({
auth,
pages: [
{
id: 'page:page_1',
auth: 'public',
auth: { public: true },
blockId: 'page_1',
type: 'Context',
meta: {
@ -1350,7 +1328,7 @@ describe('build requests', () => {
requests: [
{
id: 'request:page_1:page_1:request_1',
auth: 'public',
auth: { public: true },
contextId: 'page_1',
requestId: 'request_1',
},
@ -1420,11 +1398,11 @@ describe('build requests', () => {
test('multiple requests', async () => {
const components = {
auth,
pages: [
{
id: 'page_1',
type: 'Context',
auth,
requests: [
{
id: 'request_1',
@ -1438,11 +1416,10 @@ describe('build requests', () => {
};
const res = await buildPages({ components, context });
expect(res).toEqual({
auth,
pages: [
{
id: 'page:page_1',
auth: 'public',
auth: { public: true },
operators: [],
pageId: 'page_1',
blockId: 'page_1',
@ -1451,13 +1428,13 @@ describe('build requests', () => {
requests: [
{
id: 'request:page_1:page_1:request_1',
auth: 'public',
auth: { public: true },
requestId: 'request_1',
contextId: 'page_1',
},
{
id: 'request:page_1:page_1:request_2',
auth: 'public',
auth: { public: true },
requestId: 'request_2',
contextId: 'page_1',
},
@ -1470,11 +1447,11 @@ describe('build requests', () => {
test('add user defined loading to meta', async () => {
const components = {
auth,
pages: [
{
id: 'page_1',
type: 'Context',
auth,
loading: {
custom: true,
},
@ -1492,11 +1469,10 @@ test('add user defined loading to meta', async () => {
};
const res = await buildPages({ components, context });
expect(res).toEqual({
auth,
pages: [
{
id: 'page:page_1',
auth: 'public',
auth: { public: true },
operators: [],
pageId: 'page_1',
blockId: 'page_1',
@ -1548,116 +1524,12 @@ test('add user defined loading to meta', async () => {
});
describe('auth field', () => {
test('default auth to page on components.auth.default', async () => {
test('set auth to request', async () => {
const components = {
auth: {
set: 'setting',
default: 'defaulted',
include: [],
},
pages: [
{
id: 'page_1',
type: 'Context',
},
{
id: 'page_2',
type: 'Context',
},
],
};
const res = await buildPages({ components, context });
expect(res).toEqual({
auth: {
set: 'setting',
default: 'defaulted',
include: [],
},
pages: [
{
id: 'page:page_1',
auth: 'defaulted',
operators: [],
pageId: 'page_1',
blockId: 'page_1',
type: 'Context',
meta: outputMetas.Context,
requests: [],
},
{
id: 'page:page_2',
auth: 'defaulted',
operators: [],
pageId: 'page_2',
blockId: 'page_2',
type: 'Context',
meta: outputMetas.Context,
requests: [],
},
],
});
});
test('set auth to page in components.auth.include', async () => {
const components = {
auth: {
set: 'setting',
default: 'defaulted',
include: ['page_2'],
},
pages: [
{
id: 'page_1',
type: 'Context',
},
{
id: 'page_2',
type: 'Context',
},
],
};
const res = await buildPages({ components, context });
expect(res).toEqual({
auth: {
set: 'setting',
default: 'defaulted',
include: ['page_2'],
},
pages: [
{
id: 'page:page_1',
auth: 'defaulted',
operators: [],
pageId: 'page_1',
blockId: 'page_1',
type: 'Context',
meta: outputMetas.Context,
requests: [],
},
{
id: 'page:page_2',
auth: 'setting',
operators: [],
pageId: 'page_2',
blockId: 'page_2',
type: 'Context',
meta: outputMetas.Context,
requests: [],
},
],
});
});
test('default auth to requests on components.auth.default', async () => {
const components = {
auth: {
set: 'setting',
default: 'defaulted',
include: [],
},
pages: [
{
id: 'page_1',
auth: { public: true },
type: 'Context',
requests: [
{
@ -1668,6 +1540,7 @@ describe('auth field', () => {
{
id: 'page_2',
type: 'Context',
auth: { public: false },
requests: [
{
id: 'request_2',
@ -1678,15 +1551,10 @@ describe('auth field', () => {
};
const res = await buildPages({ components, context });
expect(res).toEqual({
auth: {
set: 'setting',
default: 'defaulted',
include: [],
},
pages: [
{
id: 'page:page_1',
auth: 'defaulted',
auth: { public: true },
operators: [],
pageId: 'page_1',
blockId: 'page_1',
@ -1695,7 +1563,7 @@ describe('auth field', () => {
requests: [
{
id: 'request:page_1:page_1:request_1',
auth: 'defaulted',
auth: { public: true },
requestId: 'request_1',
contextId: 'page_1',
},
@ -1703,7 +1571,7 @@ describe('auth field', () => {
},
{
id: 'page:page_2',
auth: 'defaulted',
auth: { public: false },
operators: [],
pageId: 'page_2',
blockId: 'page_2',
@ -1712,81 +1580,7 @@ describe('auth field', () => {
requests: [
{
id: 'request:page_2:page_2:request_2',
auth: 'defaulted',
requestId: 'request_2',
contextId: 'page_2',
},
],
},
],
});
});
test('set auth to requests in components.auth.include', async () => {
const components = {
auth: {
set: 'setting',
default: 'defaulted',
include: ['page_1'],
},
pages: [
{
id: 'page_1',
type: 'Context',
requests: [
{
id: 'request_1',
},
],
},
{
id: 'page_2',
type: 'Context',
requests: [
{
id: 'request_2',
},
],
},
],
};
const res = await buildPages({ components, context });
expect(res).toEqual({
auth: {
set: 'setting',
default: 'defaulted',
include: ['page_1'],
},
pages: [
{
id: 'page:page_1',
auth: 'setting',
operators: [],
pageId: 'page_1',
blockId: 'page_1',
type: 'Context',
meta: outputMetas.Context,
requests: [
{
id: 'request:page_1:page_1:request_1',
auth: 'setting',
requestId: 'request_1',
contextId: 'page_1',
},
],
},
{
id: 'page:page_2',
auth: 'defaulted',
operators: [],
pageId: 'page_2',
blockId: 'page_2',
type: 'Context',
meta: outputMetas.Context,
requests: [
{
id: 'request:page_2:page_2:request_2',
auth: 'defaulted',
auth: { public: false },
requestId: 'request_2',
contextId: 'page_2',
},
@ -1800,11 +1594,11 @@ describe('auth field', () => {
describe('web operators', () => {
test('set empty operators array for every context', async () => {
const components = {
auth,
pages: [
{
id: 'page_1',
type: 'Context',
auth,
blocks: [
{
id: 'context_1',
@ -1833,6 +1627,7 @@ describe('web operators', () => {
{
id: 'page_2',
type: 'Context',
auth,
},
],
};
@ -1845,11 +1640,11 @@ describe('web operators', () => {
test('set all operators for context', async () => {
const components = {
auth,
pages: [
{
id: 'page_1',
type: 'Context',
auth,
properties: {
a: { _c_op_1: {} },
},
@ -1883,11 +1678,11 @@ describe('web operators', () => {
test('exclude requests operators', async () => {
const components = {
auth,
pages: [
{
id: 'page_1',
type: 'Context',
auth,
requests: [
{
id: 'request_1',
@ -1929,11 +1724,11 @@ describe('web operators', () => {
test('set operators specific to multiple contexts', async () => {
const components = {
auth,
pages: [
{
id: 'page_1',
type: 'Context',
auth,
properties: {
a: { _c_op_1: {} },
},

View File

@ -0,0 +1,39 @@
/*
Copyright 2020-2021 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { type } from '@lowdefy/helpers';
function buildRequests(block, blockContext) {
if (!type.isNone(block.requests)) {
if (!type.isArray(block.requests)) {
throw new Error(
`Requests is not an array at ${block.blockId} on page ${
blockContext.pageId
}. Received ${JSON.stringify(block.requests)}`
);
}
block.requests.forEach((request) => {
request.auth = blockContext.auth;
request.requestId = request.id;
request.contextId = blockContext.contextId;
request.id = `request:${blockContext.pageId}:${blockContext.contextId}:${request.id}`;
blockContext.requests.push(request);
});
delete block.requests;
}
}
export default buildRequests;

View File

@ -0,0 +1,41 @@
/*
Copyright 2020-2021 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { type } from '@lowdefy/helpers';
async function checkPageIsContext(page, metaLoader) {
if (type.isNone(page.type)) {
throw new Error(`Page type is not defined at ${page.pageId}.`);
}
if (!type.isString(page.type)) {
throw new Error(
`Page type is not a string at ${page.pageId}. Received ${JSON.stringify(page.type)}`
);
}
const meta = await metaLoader.load(page.type);
if (!meta) {
throw new Error(
`Invalid block type at page ${page.pageId}. Received ${JSON.stringify(page.type)}`
);
}
if (meta.category !== 'context') {
throw new Error(
`Page ${page.pageId} is not of category "context". Received ${JSON.stringify(page.type)}`
);
}
}
export default checkPageIsContext;

View File

@ -0,0 +1,56 @@
/*
Copyright 2020-2021 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { get, type } from '@lowdefy/helpers';
function getContextOperators(block) {
const stripContext = (_, value) => {
if (get(value, 'meta.category') === 'context') {
return null;
}
return value;
};
// eslint-disable-next-line no-unused-vars
const { requests, ...webBlock } = block;
webBlock.areas = JSON.parse(JSON.stringify(webBlock.areas || {}), stripContext);
const operators = new Set();
const pushOperators = (_, value) => {
if (type.isObject(value) && Object.keys(value).length === 1) {
const key = Object.keys(value)[0];
const [op] = key.split('.');
const operator = op.replace(/^(_+)/gm, '_');
if (operator.length > 1 && operator[0] === '_') {
operators.add(operator);
}
}
return value;
};
JSON.parse(JSON.stringify(webBlock), pushOperators);
return [...operators];
}
function fillContextOperators(block) {
if (get(block, 'meta.category') === 'context') {
block.operators = getContextOperators(block);
}
Object.keys(block.areas || {}).forEach((key) => {
block.areas[key].blocks.map((blk) => {
fillContextOperators(blk);
});
});
}
export default fillContextOperators;

View File

@ -0,0 +1,54 @@
/*
Copyright 2020-2021 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { type } from '@lowdefy/helpers';
async function setBlockMeta(block, metaLoader, pageId) {
if (type.isNone(block.type)) {
throw new Error(`Block type is not defined at ${block.blockId} on page ${pageId}.`);
}
if (!type.isString(block.type)) {
throw new Error(
`Block type is not a string at ${block.blockId} on page ${pageId}. Received ${JSON.stringify(
block.type
)}`
);
}
const meta = await metaLoader.load(block.type);
if (!meta) {
throw new Error(
`Invalid Block type at ${block.blockId} on page ${pageId}. Received ${JSON.stringify(
block.type
)}`
);
}
const { category, loading, moduleFederation, valueType } = meta;
block.meta = { category, loading, moduleFederation };
if (category === 'input') {
block.meta.valueType = valueType;
}
if (category === 'list') {
// include valueType to ensure block has value on init
block.meta.valueType = 'array';
}
// Add user defined loading
if (block.loading) {
block.meta.loading = block.loading;
}
}
export default setBlockMeta;

View File

@ -31,15 +31,15 @@ beforeEach(() => {
test('empty components', async () => {
const components = {
version: '1.0.0',
lowdefy: '1.0.0',
};
await testSchema({ components, context });
expect().toBe();
expect(mockLogWarn.mock.calls).toEqual([]);
});
test('page auth config', async () => {
const components = {
version: '1.0.0',
lowdefy: '1.0.0',
config: {
auth: {
pages: {
@ -50,12 +50,12 @@ test('page auth config', async () => {
},
};
await testSchema({ components, context });
expect().toBe();
expect(mockLogWarn.mock.calls).toEqual([]);
});
test('app schema', async () => {
const components = {
version: '1.0.0',
lowdefy: '1.0.0',
connections: [
{
id: 'postman',
@ -86,7 +86,7 @@ test('app schema', async () => {
],
};
testSchema({ components, context });
expect().toBe();
expect(mockLogWarn.mock.calls).toEqual([]);
});
test('invalid schema', async () => {

View File

@ -17,8 +17,10 @@
*/
import { type } from '@lowdefy/helpers';
import { validate } from '@lowdefy/ajv';
import lowdefySchema from '../lowdefySchema.json';
async function buildConfig({ components }) {
async function validateConfig({ components }) {
if (type.isNone(components.config)) {
components.config = {};
}
@ -31,6 +33,13 @@ async function buildConfig({ components }) {
if (type.isNone(components.config.auth.pages)) {
components.config.auth.pages = {};
}
if (type.isNone(components.config.auth.pages.roles)) {
components.config.auth.pages.roles = {};
}
validate({
schema: lowdefySchema.definitions.authConfig,
data: components.config.auth,
});
if (
(components.config.auth.pages.protected === true &&
components.config.auth.pages.public === true) ||
@ -47,27 +56,7 @@ async function buildConfig({ components }) {
if (components.config.auth.pages.public === false) {
throw new Error('Public pages can not be set to false.');
}
components.auth = {};
if (
type.isArray(components.config.auth.pages.public) ||
components.config.auth.pages.protected === true
) {
components.auth.include = components.config.auth.pages.public || [];
components.auth.set = 'public';
components.auth.default = 'protected';
} else if (
type.isArray(components.config.auth.pages.protected) ||
components.config.auth.pages.public === true
) {
components.auth.include = components.config.auth.pages.protected || [];
components.auth.set = 'protected';
components.auth.default = 'public';
} else {
components.auth.include = [];
components.auth.set = 'public';
components.auth.default = 'public';
}
return components;
}
export default buildConfig;
export default validateConfig;

View File

@ -0,0 +1,113 @@
/*
Copyright 2020-2021 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import validateConfig from './validateConfig';
import testContext from '../test/testContext';
const context = testContext();
test('validateConfig config not an object', async () => {
const components = {
config: 'config',
};
await expect(validateConfig({ components, context })).rejects.toThrow('Config is not an object.');
});
test('validateConfig config invalid auth config', async () => {
let components = {
config: {
auth: {
pages: {
protected: {},
},
},
},
};
await expect(validateConfig({ components, context })).rejects.toThrow(
'App "config.auth.pages.protected.$" should be an array of strings.'
);
components = {
config: {
auth: {
pages: {
roles: ['a'],
},
},
},
};
await expect(validateConfig({ components, context })).rejects.toThrow(
'App "config.auth.pages.roles" should be an object.'
);
});
test('validateConfig config error when both protected and public pages are both arrays', async () => {
const components = {
config: {
auth: {
pages: {
protected: [],
public: [],
},
},
},
};
await expect(validateConfig({ components, context })).rejects.toThrow(
'Protected and public pages are mutually exclusive. When protected pages are listed, all unlisted pages are public by default and visa versa.'
);
});
test('validateConfig config error when both protected and public pages are true', async () => {
const components = {
config: {
auth: {
pages: {
protected: true,
public: true,
},
},
},
};
await expect(validateConfig({ components, context })).rejects.toThrow(
'Protected and public pages are mutually exclusive. When protected pages are listed, all unlisted pages are public by default and visa versa.'
);
});
test('validateConfig config error when protected or public are false.', async () => {
let components = {
config: {
auth: {
pages: {
protected: false,
},
},
},
};
await expect(validateConfig({ components, context })).rejects.toThrow(
'Protected pages can not be set to false.'
);
components = {
config: {
auth: {
pages: {
public: false,
},
},
},
};
await expect(validateConfig({ components, context })).rejects.toThrow(
'Public pages can not be set to false.'
);
});

View File

@ -4,7 +4,7 @@
"type": "object",
"title": "Lowdefy App Schema",
"definitions": {
"connection": {
"action": {
"type": "object",
"additionalProperties": false,
"required": ["id", "type"],
@ -12,27 +12,141 @@
"id": {
"type": "string",
"errorMessage": {
"type": "Connection \"id\" should be a string."
"type": "Action \"id\" should be a string."
}
},
"type": {
"type": "string",
"errorMessage": {
"type": "Connection \"type\" should be a string."
"type": "Action \"type\" should be a string."
}
},
"properties": {
"type": "object",
"errorMessage": {
"type": "Connection \"properties\" should be an object."
}
}
"messages": {},
"skip": {},
"params": {}
},
"errorMessage": {
"type": "Connection should be an object.",
"type": "Action should be an object.",
"required": {
"id": "Connection should have required property \"id\".",
"type": "Connection should have required property \"type\"."
"id": "Action should have required property \"id\".",
"type": "Action should have required property \"type\"."
}
}
},
"authConfig": {
"type": "object",
"additionalProperties": false,
"errorMessage": {
"type": "App \"config.auth\" should be an object."
},
"properties": {
"openId": {
"type": "object",
"additionalProperties": false,
"errorMessage": {
"type": "App \"config.auth.openId\" should be an object."
},
"properties": {
"rolesField": {
"type": "string",
"description": ".",
"errorMessage": {
"type": "App \"config.auth.openId.rolesField\" should be a string."
}
},
"logoutFromProvider": {
"type": "boolean",
"default": false,
"description": "If true, the user will be directed to OpenID Connect provider's logout URL. This will log the user out of the provider.",
"errorMessage": {
"type": "App \"config.auth.openId.logoutFromProvider\" should be a boolean."
}
},
"logoutRedirectUri": {
"type": "string",
"description": "The URI to redirect the user to after logout.",
"errorMessage": {
"type": "App \"config.auth.openId.logoutRedirectUri\" should be a string."
}
},
"scope": {
"type": "string",
"description": "The OpenID Connect scope to request.",
"default": "openid profile email",
"errorMessage": {
"type": "App \"config.auth.openId.scope\" should be a string."
}
}
}
},
"pages": {
"type": "object",
"additionalProperties": false,
"errorMessage": {
"type": "App \"config.auth.pages\" should be an object."
},
"properties": {
"protected": {
"type": ["array", "boolean"],
"errorMessage": {
"type": "App \"config.auth.pages.protected.$\" should be an array of strings."
},
"items": {
"type": "string",
"description": "Page ids for which authentication is required. When specified, all unspecified pages will be public.",
"errorMessage": {
"type": "App \"config.auth.pages.protected.$\" should be an array of strings."
}
}
},
"public": {
"type": ["array", "boolean"],
"errorMessage": {
"type": "App \"config.auth.pages.public.$\" should be an array of strings."
},
"items": {
"type": "string",
"description": "Page ids for which authentication is not required. When specified, all unspecified pages will be protected.",
"errorMessage": {
"type": "App \"config.auth.pages.public.$\" should be an array of strings."
}
}
},
"roles": {
"type": "object",
"patternProperties": {
"^.*$": {
"type": "array",
"items": {
"type": "string"
},
"errorMessage": {
"type": "App \"config.auth.pages.roles.[role]\" should be an array of strings."
}
}
},
"errorMessage": {
"type": "App \"config.auth.pages.roles\" should be an object."
}
}
}
},
"jwt": {
"type": "object",
"additionalProperties": false,
"errorMessage": {
"type": "App \"config.auth.jwt\" should be an object."
},
"properties": {
"expiresIn": {
"type": ["string", "number"],
"default": "4h",
"description": "The length of time a user token should be valid. Can be expressed as a number in seconds, or a vercel/ms string (https://github.com/vercel/ms)",
"errorMessage": {
"type": "App \"config.auth.jwt.expiresIn\" should be a string or number."
}
}
}
}
}
},
@ -160,46 +274,7 @@
}
}
},
"request": {
"type": "object",
"additionalProperties": false,
"required": ["id", "type", "connectionId"],
"properties": {
"id": {
"type": "string",
"errorMessage": {
"type": "Request \"id\" should be a string."
}
},
"type": {
"type": "string",
"errorMessage": {
"type": "Request \"type\" should be a string."
}
},
"connectionId": {
"type": "string",
"errorMessage": {
"type": "Request \"connectionId\" should be a string."
}
},
"properties": {
"type": "object",
"errorMessage": {
"type": "Request \"properties\" should be an object."
}
}
},
"errorMessage": {
"type": "Request should be an object.",
"required": {
"id": "Request should have required property \"id\".",
"type": "Request should have required property \"type\".",
"connectionId": "Request should have required property \"connectionId\"."
}
}
},
"menuLink": {
"connection": {
"type": "object",
"additionalProperties": false,
"required": ["id", "type"],
@ -207,39 +282,61 @@
"id": {
"type": "string",
"errorMessage": {
"type": "MenuLink \"id\" should be a string."
"type": "Connection \"id\" should be a string."
}
},
"type": {
"type": "string",
"errorMessage": {
"type": "MenuLink \"type\" should be a string."
}
},
"pageId": {
"type": "string",
"errorMessage": {
"type": "MenuLink \"pageId\" should be a string."
}
},
"url": {
"type": "string",
"errorMessage": {
"type": "MenuLink \"url\" should be a string."
"type": "Connection \"type\" should be a string."
}
},
"properties": {
"type": "object",
"errorMessage": {
"type": "MenuLink \"properties\" should be an object."
"type": "Connection \"properties\" should be an object."
}
}
},
"errorMessage": {
"type": "MenuLink should be an object.",
"type": "Connection should be an object.",
"required": {
"id": "MenuLink should have required property \"id\".",
"type": "MenuLink should have required property \"type\"."
"id": "Connection should have required property \"id\".",
"type": "Connection should have required property \"type\"."
}
}
},
"menu": {
"type": "object",
"additionalProperties": false,
"required": ["id"],
"properties": {
"id": {
"type": "string",
"errorMessage": {
"type": "Menu \"id\" should be a string."
}
},
"properties": {
"type": "object",
"errorMessage": {
"type": "Menu \"properties\" should be an object."
}
},
"links": {
"type": "array",
"items": {
"$ref": "#/definitions/menuItem"
},
"errorMessage": {
"type": "Menu \"links\" should be an array."
}
}
},
"errorMessage": {
"type": "Menu should be an object.",
"required": {
"id": "Menu should have required property \"id\"."
}
}
},
@ -294,41 +391,7 @@
}
]
},
"menu": {
"type": "object",
"additionalProperties": false,
"required": ["id"],
"properties": {
"id": {
"type": "string",
"errorMessage": {
"type": "Menu \"id\" should be a string."
}
},
"properties": {
"type": "object",
"errorMessage": {
"type": "Menu \"properties\" should be an object."
}
},
"links": {
"type": "array",
"items": {
"$ref": "#/definitions/menuItem"
},
"errorMessage": {
"type": "Menu \"links\" should be an array."
}
}
},
"errorMessage": {
"type": "Menu should be an object.",
"required": {
"id": "Menu should have required property \"id\"."
}
}
},
"action": {
"menuLink": {
"type": "object",
"additionalProperties": false,
"required": ["id", "type"],
@ -336,27 +399,82 @@
"id": {
"type": "string",
"errorMessage": {
"type": "Action \"id\" should be a string."
"type": "MenuLink \"id\" should be a string."
}
},
"type": {
"type": "string",
"errorMessage": {
"type": "Action \"type\" should be a string."
"type": "MenuLink \"type\" should be a string."
}
},
"messages": {},
"skip": {},
"params": {}
"pageId": {
"type": "string",
"errorMessage": {
"type": "MenuLink \"pageId\" should be a string."
}
},
"url": {
"type": "string",
"errorMessage": {
"type": "MenuLink \"url\" should be a string."
}
},
"properties": {
"type": "object",
"errorMessage": {
"type": "MenuLink \"properties\" should be an object."
}
}
},
"errorMessage": {
"type": "Action should be an object.",
"type": "MenuLink should be an object.",
"required": {
"id": "Action should have required property \"id\".",
"type": "Action should have required property \"type\"."
"id": "MenuLink should have required property \"id\".",
"type": "MenuLink should have required property \"type\"."
}
}
},
"request": {
"type": "object",
"additionalProperties": false,
"required": ["id", "type", "connectionId"],
"properties": {
"id": {
"type": "string",
"errorMessage": {
"type": "Request \"id\" should be a string."
}
},
"type": {
"type": "string",
"errorMessage": {
"type": "Request \"type\" should be a string."
}
},
"connectionId": {
"type": "string",
"errorMessage": {
"type": "Request \"connectionId\" should be a string."
}
},
"properties": {
"type": "object",
"errorMessage": {
"type": "Request \"properties\" should be an object."
}
}
},
"errorMessage": {
"type": "Request should be an object.",
"required": {
"id": "Request should have required property \"id\".",
"type": "Request should have required property \"type\".",
"connectionId": "Request should have required property \"connectionId\"."
}
}
}
},
"additionalProperties": false,
"required": ["lowdefy"],
@ -400,97 +518,7 @@
}
},
"auth": {
"type": "object",
"additionalProperties": false,
"errorMessage": {
"type": "App \"config.auth\" should be an object."
},
"properties": {
"openId": {
"type": "object",
"additionalProperties": false,
"errorMessage": {
"type": "App \"config.auth.openId\" should be an object."
},
"properties": {
"logoutFromProvider": {
"type": "boolean",
"default": false,
"description": "If true, the user will be directed to OpenID Connect provider's logout URL. This will log the user out of the provider.",
"errorMessage": {
"type": "App \"config.auth.openId.logoutFromProvider\" should be a boolean."
}
},
"logoutRedirectUri": {
"type": "string",
"description": "The URI to redirect the user to after logout.",
"errorMessage": {
"type": "App \"config.auth.openId.logoutRedirectUri\" should be a string."
}
},
"scope": {
"type": "string",
"description": "The OpenID Connect scope to request.",
"default": "openid profile email",
"errorMessage": {
"type": "App \"config.auth.openId.scope\" should be a string."
}
}
}
},
"pages": {
"type": "object",
"additionalProperties": false,
"errorMessage": {
"type": "App \"config.auth.pages\" should be an object."
},
"properties": {
"protected": {
"type": ["array", "boolean"],
"errorMessage": {
"type": "App \"config.auth.pages.protected.$\" should be an array of strings."
},
"items": {
"type": "string",
"description": "Page ids for which authentication is required. When specified, all unspecified pages will be public.",
"errorMessage": {
"type": "App \"config.auth.pages.protected.$\" should be an array of strings."
}
}
},
"public": {
"type": ["array", "boolean"],
"errorMessage": {
"type": "App \"config.auth.pages.public.$\" should be an array of strings."
},
"items": {
"type": "string",
"description": "Page ids for which authentication is not required. When specified, all unspecified pages will be protected.",
"errorMessage": {
"type": "App \"config.auth.pages.public.$\" should be an array of strings."
}
}
}
}
},
"jwt": {
"type": "object",
"additionalProperties": false,
"errorMessage": {
"type": "App \"config.auth.jwt\" should be an object."
},
"properties": {
"expiresIn": {
"type": ["string", "number"],
"default": "4h",
"description": "The length of time a user token should be valid. Can be expressed as a number in seconds, or a vercel/ms string (https://github.com/vercel/ms)",
"errorMessage": {
"type": "App \"config.auth.jwt.expiresIn\" should be a string or number."
}
}
}
}
}
"$ref": "#/definitions/authConfig"
}
}
},

View File

@ -35,10 +35,13 @@ function createContext(config) {
bootstrapContext.setHeader = (key, value) => res.set(key, value);
bootstrapContext.headers = req.headers;
bootstrapContext.host =
// TODO: Might not be necessary if all apps are express based
get(bootstrapContext.headers, 'Host') || get(bootstrapContext.headers, 'host');
bootstrapContext.getLoader = createGetLoader(bootstrapContext);
bootstrapContext.getController = createGetController(bootstrapContext);
bootstrapContext.user = await verifyAccessToken(bootstrapContext);
const { user, roles } = await verifyAccessToken(bootstrapContext);
bootstrapContext.user = user;
bootstrapContext.roles = roles;
return {
getController: bootstrapContext.getController,
logger,

View File

@ -19,7 +19,14 @@
import { get } from '@lowdefy/helpers';
import cookie from 'cookie';
async function verifyAccessToken({ development, headers, getController, gqlUri, setHeader }) {
async function verifyAccessToken({
development,
headers,
getController,
getLoader,
gqlUri,
setHeader,
}) {
const cookieHeader = get(headers, 'Cookie') || get(headers, 'cookie') || '';
const { authorization } = cookie.parse(cookieHeader);
if (!authorization) return {};
@ -33,7 +40,14 @@ async function verifyAccessToken({ development, headers, getController, gqlUri,
lowdefy_access_token,
...user
} = await tokenController.verifyAccessToken(authorization);
return user;
const componentLoader = getLoader('component');
const appConfig = await componentLoader.load('config');
const rolesField = get(appConfig, 'auth.openId.rolesField');
let roles = [];
if (rolesField) {
roles = get(user, rolesField);
}
return { user, roles };
} catch (error) {
const setCookieHeader = cookie.serialize('authorization', '', {
httpOnly: true,

View File

@ -24,7 +24,14 @@ import { AuthenticationError, TokenExpiredError } from '../context/errors';
const setHeader = jest.fn();
const getSecrets = () => ({ JWT_SECRET: 'JWT_SECRET' });
const createCookieHeader = async ({ expired } = {}) => {
const mockLoadComponent = jest.fn();
const loaders = {
component: {
load: mockLoadComponent,
},
};
const createCookieHeader = async ({ expired, customRoles } = {}) => {
const tokenController = createTokenController({
getSecrets,
host: 'host',
@ -36,7 +43,11 @@ const createCookieHeader = async ({ expired } = {}) => {
}),
}),
});
const accessToken = await tokenController.issueAccessToken({ sub: 'sub', email: 'email' });
const accessToken = await tokenController.issueAccessToken({
sub: 'sub',
email: 'email',
customRoles,
});
return cookie.serialize('authorization', accessToken, {
httpOnly: true,
path: '/api/graphql',
@ -67,19 +78,49 @@ test('empty cookie header', async () => {
});
test('valid authorization cookie', async () => {
mockLoadComponent.mockImplementation(() => ({}));
const cookie = await createCookieHeader();
let bootstrapContext = testBootstrapContext({
headers: { cookie },
getSecrets,
loaders,
});
let user = await verifyAccessToken(bootstrapContext);
expect(user).toEqual({ sub: 'sub', email: 'email' });
let res = await verifyAccessToken(bootstrapContext);
expect(res).toEqual({ user: { sub: 'sub', email: 'email' }, roles: [] });
bootstrapContext = testBootstrapContext({
headers: { Cookie: cookie },
getSecrets,
loaders,
});
res = await verifyAccessToken(bootstrapContext);
expect(res).toEqual({ user: { sub: 'sub', email: 'email' }, roles: [] });
});
test('valid authorization cookie with roles', async () => {
mockLoadComponent.mockImplementation(() => ({
auth: { openId: { rolesField: 'customRoles' } },
}));
const cookie = await createCookieHeader({ customRoles: ['role1', 'role2'] });
let bootstrapContext = testBootstrapContext({
headers: { cookie },
getSecrets,
loaders,
});
let res = await verifyAccessToken(bootstrapContext);
expect(res).toEqual({
user: { sub: 'sub', email: 'email', customRoles: ['role1', 'role2'] },
roles: ['role1', 'role2'],
});
bootstrapContext = testBootstrapContext({
headers: { Cookie: cookie },
getSecrets,
loaders,
});
res = await verifyAccessToken(bootstrapContext);
expect(res).toEqual({
user: { sub: 'sub', email: 'email', customRoles: ['role1', 'role2'] },
roles: ['role1', 'role2'],
});
user = await verifyAccessToken(bootstrapContext);
expect(user).toEqual({ sub: 'sub', email: 'email' });
});
test('invalid authorization cookie', async () => {

View File

@ -14,16 +14,21 @@
limitations under the License.
*/
import { get, type } from '@lowdefy/helpers';
import { ServerError } from '../context/errors';
class AuthorizationController {
constructor({ user }) {
this.authenticated = !!user.sub;
constructor({ user, roles }) {
this.authenticated = type.isString(get(user, 'sub'));
this.roles = roles || [];
}
authorize({ auth }) {
if (auth === 'public') return true;
if (auth === 'protected') {
if (auth.public === true) return true;
if (auth.public === false) {
if (auth.roles) {
return this.authenticated && auth.roles.some((role) => this.roles.includes(role));
}
return this.authenticated;
}
throw new ServerError('Invalid auth configuration');

View File

@ -25,26 +25,69 @@ test('authenticated true', async () => {
expect(authController.authenticated).toBe(true);
});
test('authenticated true', async () => {
test('authenticated false', async () => {
const context = testBootstrapContext({});
const authController = createAuthorizationController(context);
expect(authController.authenticated).toBe(false);
});
test('authorize with user', async () => {
const context = testBootstrapContext({ user: { sub: 'sub' } });
const authController = createAuthorizationController(context);
expect(authController.authorize({ auth: 'protected' })).toBe(true);
expect(authController.authorize({ auth: 'public' })).toBe(true);
expect(() => authController.authorize({ auth: 'other' })).toThrow(ServerError);
expect(() => authController.authorize({})).toThrow(ServerError);
test('authorize public object', async () => {
const auth = { public: true };
let context = testBootstrapContext({});
let authController = createAuthorizationController(context);
expect(authController.authorize({ auth })).toBe(true);
context = testBootstrapContext({ user: { sub: 'sub' } });
authController = createAuthorizationController(context);
expect(authController.authorize({ auth })).toBe(true);
});
test('authorize without user', async () => {
const context = testBootstrapContext({ user: {} });
const authController = createAuthorizationController(context);
expect(authController.authorize({ auth: 'protected' })).toBe(false);
expect(authController.authorize({ auth: 'public' })).toBe(true);
expect(() => authController.authorize({ auth: 'other' })).toThrow(ServerError);
expect(() => authController.authorize({})).toThrow(ServerError);
test('authorize protected object, no roles', async () => {
const auth = { public: false };
let context = testBootstrapContext({});
let authController = createAuthorizationController(context);
expect(authController.authorize({ auth })).toBe(false);
context = testBootstrapContext({ user: { sub: 'sub' } });
authController = createAuthorizationController(context);
expect(authController.authorize({ auth })).toBe(true);
});
test('authorize role protected object', async () => {
const auth = { public: false, roles: ['role1'] };
let context = testBootstrapContext({});
let authController = createAuthorizationController(context);
expect(authController.authorize({ auth })).toBe(false);
context = testBootstrapContext({ user: { sub: 'sub' } });
authController = createAuthorizationController(context);
expect(authController.authorize({ auth })).toBe(false);
context = testBootstrapContext({ user: { sub: 'sub' }, roles: [] });
authController = createAuthorizationController(context);
expect(authController.authorize({ auth })).toBe(false);
context = testBootstrapContext({ user: { sub: 'sub' }, roles: ['role2'] });
authController = createAuthorizationController(context);
expect(authController.authorize({ auth })).toBe(false);
context = testBootstrapContext({ user: { sub: 'sub' }, roles: ['role1'] });
authController = createAuthorizationController(context);
expect(authController.authorize({ auth })).toBe(true);
context = testBootstrapContext({ user: { sub: 'sub' }, roles: ['role1', 'role2'] });
authController = createAuthorizationController(context);
expect(authController.authorize({ auth })).toBe(true);
});
test('invalid auth config', async () => {
const context = testBootstrapContext({});
const authController = createAuthorizationController(context);
expect(() => authController.authorize({ auth: { other: 'value' } })).toThrow(ServerError);
expect(() => authController.authorize({ auth: {} })).toThrow(ServerError);
expect(() => authController.authorize({})).toThrow();
expect(() => authController.authorize()).toThrow();
});

View File

@ -76,14 +76,14 @@ test('getMenus, menu with configured home page id', async () => {
id: 'menuitem:default:0',
menuItemId: '0',
type: 'MenuGroup',
auth: 'public',
auth: { public: true },
links: [
{
id: 'menuitem:default:1',
menuItemId: '1',
type: 'MenuLink',
pageId: 'page',
auth: 'public',
auth: { public: true },
},
],
},
@ -109,14 +109,14 @@ test('getMenus, menu with configured home page id', async () => {
id: 'menuitem:default:0',
menuItemId: '0',
type: 'MenuGroup',
auth: 'public',
auth: { public: true },
links: [
{
id: 'menuitem:default:1',
menuItemId: '1',
type: 'MenuLink',
pageId: 'page',
auth: 'public',
auth: { public: true },
},
],
},
@ -139,7 +139,7 @@ test('getMenus, get homePageId at first level', async () => {
menuItemId: '0',
type: 'MenuLink',
pageId: 'page',
auth: 'public',
auth: { public: true },
},
],
},
@ -162,7 +162,7 @@ test('getMenus, get homePageId at first level', async () => {
menuItemId: '0',
type: 'MenuLink',
pageId: 'page',
auth: 'public',
auth: { public: true },
},
],
},
@ -182,14 +182,14 @@ test('getMenus, get homePageId at second level', async () => {
id: 'menuitem:default:0',
menuItemId: '0',
type: 'MenuGroup',
auth: 'public',
auth: { public: true },
links: [
{
id: 'menuitem:default:1',
menuItemId: '1',
type: 'MenuLink',
pageId: 'page',
auth: 'public',
auth: { public: true },
},
],
},
@ -213,14 +213,14 @@ test('getMenus, get homePageId at second level', async () => {
id: 'menuitem:default:0',
menuItemId: '0',
type: 'MenuGroup',
auth: 'public',
auth: { public: true },
links: [
{
id: 'menuitem:default:1',
menuItemId: '1',
type: 'MenuLink',
pageId: 'page',
auth: 'public',
auth: { public: true },
},
],
},
@ -242,20 +242,20 @@ test('getMenus, get homePageId at third level', async () => {
id: 'menuitem:default:0',
menuItemId: '0',
type: 'MenuGroup',
auth: 'public',
auth: { public: true },
links: [
{
id: 'menuitem:default:1',
menuItemId: '1',
type: 'MenuGroup',
auth: 'public',
auth: { public: true },
links: [
{
id: 'menuitem:default:2',
menuItemId: '2',
type: 'MenuLink',
pageId: 'page',
auth: 'public',
auth: { public: true },
},
],
},
@ -281,20 +281,20 @@ test('getMenus, get homePageId at third level', async () => {
id: 'menuitem:default:0',
menuItemId: '0',
type: 'MenuGroup',
auth: 'public',
auth: { public: true },
links: [
{
id: 'menuitem:default:1',
menuItemId: '1',
type: 'MenuGroup',
auth: 'public',
auth: { public: true },
links: [
{
id: 'menuitem:default:2',
menuItemId: '2',
type: 'MenuLink',
pageId: 'page',
auth: 'public',
auth: { public: true },
},
],
},
@ -319,7 +319,7 @@ test('getMenus, no default menu, no configured homepage', async () => {
menuItemId: '0',
type: 'MenuLink',
pageId: 'page',
auth: 'public',
auth: { public: true },
},
],
},
@ -342,7 +342,7 @@ test('getMenus, no default menu, no configured homepage', async () => {
menuItemId: '0',
type: 'MenuLink',
pageId: 'page',
auth: 'public',
auth: { public: true },
},
],
},
@ -363,7 +363,7 @@ test('getMenus, more than 1 menu, no configured homepage', async () => {
menuItemId: '0',
type: 'MenuLink',
pageId: 'other-page',
auth: 'public',
auth: { public: true },
},
],
},
@ -375,7 +375,7 @@ test('getMenus, more than 1 menu, no configured homepage', async () => {
menuItemId: '0',
type: 'MenuLink',
pageId: 'default-page',
auth: 'public',
auth: { public: true },
},
],
},
@ -398,7 +398,7 @@ test('getMenus, more than 1 menu, no configured homepage', async () => {
menuItemId: '0',
type: 'MenuLink',
pageId: 'other-page',
auth: 'public',
auth: { public: true },
},
],
},
@ -410,7 +410,7 @@ test('getMenus, more than 1 menu, no configured homepage', async () => {
menuItemId: '0',
type: 'MenuLink',
pageId: 'default-page',
auth: 'public',
auth: { public: true },
},
],
},
@ -460,33 +460,33 @@ describe('filter menus', () => {
menuItemId: '1',
type: 'MenuLink',
pageId: 'page',
auth: 'protected',
auth: { public: false },
},
{
id: 'menuitem:default:2',
menuItemId: '2',
type: 'MenuGroup',
auth: 'public',
auth: { public: true },
links: [
{
id: 'menuitem:default:3',
menuItemId: '3',
type: 'MenuLink',
pageId: 'page',
auth: 'protected',
auth: { public: false },
},
{
id: 'menuitem:default:4',
menuItemId: '4',
type: 'MenuGroup',
auth: 'public',
auth: { public: true },
links: [
{
id: 'menuitem:default:5',
menuItemId: '5',
type: 'MenuLink',
pageId: 'page',
auth: 'protected',
auth: { public: false },
},
],
},
@ -502,7 +502,7 @@ describe('filter menus', () => {
menuItemId: '1',
type: 'MenuLink',
pageId: 'page',
auth: 'protected',
auth: { public: false },
},
],
},
@ -542,33 +542,33 @@ describe('filter menus', () => {
menuItemId: '1',
type: 'MenuLink',
pageId: 'page',
auth: 'protected',
auth: { public: false },
},
{
id: 'menuitem:default:2',
menuItemId: '2',
type: 'MenuGroup',
auth: 'public',
auth: { public: true },
links: [
{
id: 'menuitem:default:3',
menuItemId: '3',
type: 'MenuLink',
pageId: 'page',
auth: 'protected',
auth: { public: false },
},
{
id: 'menuitem:default:4',
menuItemId: '4',
type: 'MenuGroup',
auth: 'public',
auth: { public: true },
links: [
{
id: 'menuitem:default:5',
menuItemId: '5',
type: 'MenuLink',
pageId: 'page',
auth: 'protected',
auth: { public: false },
},
],
},
@ -584,7 +584,7 @@ describe('filter menus', () => {
menuItemId: '1',
type: 'MenuLink',
pageId: 'page',
auth: 'protected',
auth: { public: false },
},
],
},
@ -607,33 +607,33 @@ describe('filter menus', () => {
menuItemId: '1',
type: 'MenuLink',
pageId: 'page',
auth: 'protected',
auth: { public: false },
},
{
id: 'menuitem:default:2',
menuItemId: '2',
type: 'MenuGroup',
auth: 'public',
auth: { public: true },
links: [
{
id: 'menuitem:default:3',
menuItemId: '3',
type: 'MenuLink',
pageId: 'page',
auth: 'protected',
auth: { public: false },
},
{
id: 'menuitem:default:4',
menuItemId: '4',
type: 'MenuGroup',
auth: 'public',
auth: { public: true },
links: [
{
id: 'menuitem:default:5',
menuItemId: '5',
type: 'MenuLink',
pageId: 'page',
auth: 'protected',
auth: { public: false },
},
],
},
@ -649,7 +649,7 @@ describe('filter menus', () => {
menuItemId: '1',
type: 'MenuLink',
pageId: 'page',
auth: 'protected',
auth: { public: false },
},
],
},
@ -670,40 +670,40 @@ describe('filter menus', () => {
menuItemId: '1',
type: 'MenuLink',
pageId: 'page',
auth: 'protected',
auth: { public: false },
},
{
id: 'menuitem:default:2',
menuItemId: '2',
type: 'MenuGroup',
auth: 'public',
auth: { public: true },
links: [
{
id: 'menuitem:default:3',
menuItemId: '3',
type: 'MenuLink',
pageId: 'page',
auth: 'protected',
auth: { public: false },
},
{
id: 'menuitem:default:4',
menuItemId: '4',
type: 'MenuLink',
pageId: 'page',
auth: 'public',
auth: { public: true },
},
{
id: 'menuitem:default:5',
menuItemId: '5',
type: 'MenuGroup',
auth: 'public',
auth: { public: true },
links: [
{
id: 'menuitem:default:6',
menuItemId: '6',
type: 'MenuLink',
pageId: 'page',
auth: 'protected',
auth: { public: false },
},
],
},
@ -711,14 +711,14 @@ describe('filter menus', () => {
id: 'menuitem:default:7',
menuItemId: '7',
type: 'MenuGroup',
auth: 'public',
auth: { public: true },
links: [
{
id: 'menuitem:default:8',
menuItemId: '8',
type: 'MenuLink',
url: 'https://lowdefy.com',
auth: 'public',
auth: { public: true },
},
],
},
@ -744,27 +744,27 @@ describe('filter menus', () => {
id: 'menuitem:default:2',
menuItemId: '2',
type: 'MenuGroup',
auth: 'public',
auth: { public: true },
links: [
{
id: 'menuitem:default:4',
menuItemId: '4',
pageId: 'page',
type: 'MenuLink',
auth: 'public',
auth: { public: true },
},
{
id: 'menuitem:default:7',
menuItemId: '7',
type: 'MenuGroup',
auth: 'public',
auth: { public: true },
links: [
{
id: 'menuitem:default:8',
menuItemId: '8',
type: 'MenuLink',
url: 'https://lowdefy.com',
auth: 'public',
auth: { public: true },
},
],
},
@ -790,7 +790,7 @@ test('Filter invalid menu item types', async () => {
menuItemId: '1',
type: 'MenuItem',
pageId: 'page',
auth: 'protected',
auth: { public: false },
},
],
},

View File

@ -35,7 +35,9 @@ test('getPage, public', async () => {
if (id === 'pageId') {
return {
id: 'page:pageId',
auth: 'public',
auth: {
public: true,
},
};
}
return null;
@ -44,7 +46,9 @@ test('getPage, public', async () => {
const res = await controller.getPage({ pageId: 'pageId' });
expect(res).toEqual({
id: 'page:pageId',
auth: 'public',
auth: {
public: true,
},
});
});
@ -53,7 +57,9 @@ test('getPage, protected, no user', async () => {
if (id === 'pageId') {
return {
id: 'page:pageId',
auth: 'protected',
auth: {
public: false,
},
};
}
return null;
@ -68,7 +74,9 @@ test('getPage, protected, with user', async () => {
if (id === 'pageId') {
return {
id: 'page:pageId',
auth: 'protected',
auth: {
public: false,
},
};
}
return null;
@ -77,7 +85,9 @@ test('getPage, protected, with user', async () => {
const res = await controller.getPage({ pageId: 'pageId' });
expect(res).toEqual({
id: 'page:pageId',
auth: 'protected',
auth: {
public: false,
},
});
});
@ -86,7 +96,9 @@ test('getPage, page does not exist', async () => {
if (id === 'pageId') {
return {
id: 'page:pageId',
auth: 'public',
auth: {
public: true,
},
};
}
return null;

View File

@ -111,7 +111,7 @@ const defaultLoadRequestImp = ({ pageId, contextId, requestId }) => {
type: 'TestRequest',
requestId: 'requestId',
connectionId: 'testConnection',
auth: 'public',
auth: { public: true },
properties: {
requestProperty: 'requestProperty',
},
@ -161,7 +161,7 @@ test('call request, protected auth with user', async () => {
type: 'TestRequest',
requestId: 'requestId',
connectionId: 'testConnection',
auth: 'protected',
auth: { public: false },
properties: {
requestProperty: 'requestProperty',
},
@ -198,7 +198,7 @@ test('call request, protected auth without user', async () => {
type: 'TestRequest',
requestId: 'requestId',
connectionId: 'testConnection',
auth: 'protected',
auth: { public: false },
properties: {
requestProperty: 'requestProperty',
},
@ -233,7 +233,7 @@ test('request does not have a connectionId', async () => {
id: 'request:pageId:contextId:requestId',
type: 'TestRequest',
requestId: 'requestId',
auth: 'public',
auth: { public: true },
properties: {
requestProperty: 'requestProperty',
},
@ -258,7 +258,7 @@ test('request is not a valid request type', async () => {
type: 'InvalidType',
requestId: 'requestId',
connectionId: 'testConnection',
auth: 'public',
auth: { public: true },
properties: {
requestProperty: 'requestProperty',
},
@ -317,7 +317,7 @@ test('deserialize inputs', async () => {
type: 'TestRequest',
requestId: 'requestId',
connectionId: 'testConnection',
auth: 'public',
auth: { public: true },
properties: {
event: { _event: true },
input: { _input: true },
@ -389,7 +389,7 @@ test('parse request properties for operators', async () => {
type: 'TestRequest',
requestId: 'requestId',
connectionId: 'testConnection',
auth: 'public',
auth: { public: true },
properties: {
input: { _input: 'value' },
event: { _event: 'value' },
@ -539,7 +539,7 @@ test('parse secrets', async () => {
type: 'TestRequest',
requestId: 'requestId',
connectionId: 'testConnection',
auth: 'public',
auth: { public: true },
properties: {
secret: { _secret: 'REQUEST' },
},
@ -574,7 +574,7 @@ test('request properties default value', async () => {
type: 'TestRequest',
requestId: 'requestId',
connectionId: 'testConnection',
auth: 'public',
auth: { public: true },
};
}
return null;
@ -632,7 +632,7 @@ test('request properties operator error', async () => {
type: 'TestRequest',
requestId: 'requestId',
connectionId: 'testConnection',
auth: 'public',
auth: { public: true },
properties: {
willError: { _state: [] },
},
@ -655,7 +655,7 @@ test('connection properties operator error', async () => {
id: 'connection:testConnection',
type: 'TestConnection',
connectionId: 'testConnection',
auth: 'public',
auth: { public: true },
properties: {
willError: { _state: [] },
},
@ -735,7 +735,7 @@ test('request properties schema error', async () => {
type: 'TestRequest',
requestId: 'requestId',
connectionId: 'testConnection',
auth: 'public',
auth: { public: true },
properties: {
schemaPropString: true,
},
@ -770,7 +770,7 @@ test('checkRead, read explicitly true', async () => {
type: 'TestRequestCheckRead',
requestId: 'requestId',
connectionId: 'testConnection',
auth: 'public',
auth: { public: true },
properties: {},
};
}
@ -813,7 +813,7 @@ test('checkRead, read explicitly false', async () => {
type: 'TestRequestCheckRead',
requestId: 'requestId',
connectionId: 'testConnection',
auth: 'public',
auth: { public: true },
properties: {},
};
}
@ -834,7 +834,7 @@ test('checkRead, read not set', async () => {
id: 'connection:testConnection',
type: 'TestConnection',
connectionId: 'testConnection',
auth: 'public',
auth: { public: true },
properties: {},
};
}
@ -847,7 +847,7 @@ test('checkRead, read not set', async () => {
type: 'TestRequestCheckRead',
requestId: 'requestId',
connectionId: 'testConnection',
auth: 'public',
auth: { public: true },
properties: {},
};
}
@ -888,7 +888,7 @@ test('checkWrite, write explicitly true', async () => {
type: 'TestRequestCheckWrite',
requestId: 'requestId',
connectionId: 'testConnection',
auth: 'public',
auth: { public: true },
properties: {},
};
}
@ -931,7 +931,7 @@ test('checkWrite, write explicitly false', async () => {
type: 'TestRequestCheckWrite',
requestId: 'requestId',
connectionId: 'testConnection',
auth: 'public',
auth: { public: true },
properties: {},
};
}
@ -964,7 +964,7 @@ test('checkWrite, write not set', async () => {
type: 'TestRequestCheckWrite',
requestId: 'requestId',
connectionId: 'testConnection',
auth: 'public',
auth: { public: true },
properties: {},
};
}

View File

@ -28,13 +28,13 @@ const mockLoadMenus = jest.fn((id) => {
{
id: 'menuitem:default:0',
type: 'MenuGroup',
auth: 'public',
auth: { public: true },
links: [
{
id: 'menuitem:default:1',
type: 'MenuLink',
pageId: 'page',
auth: 'public',
auth: { public: true },
},
],
},

View File

@ -25,7 +25,7 @@ const mockLoadPage = jest.fn((id) => {
type: 'PageHeaderMenu',
pageId: 'pageId',
blockId: 'pageId',
auth: 'public',
auth: { public: true },
};
}
return null;
@ -58,7 +58,7 @@ test('page resolver', async () => {
type: 'PageHeaderMenu',
pageId: 'pageId',
blockId: 'pageId',
auth: 'public',
auth: { public: true },
});
});
@ -75,7 +75,7 @@ test('page graphql', async () => {
type: 'PageHeaderMenu',
pageId: 'pageId',
blockId: 'pageId',
auth: 'public',
auth: { public: true },
},
});
});

View File

@ -24,6 +24,7 @@ function testBootstrapContext({
host,
loaders,
setHeader,
roles,
user,
} = {}) {
const bootstrapContext = {
@ -37,7 +38,8 @@ function testBootstrapContext({
host: host || 'host',
logger: { log: () => {} },
setHeader: setHeader || (() => {}),
user: user || {},
roles: roles,
user: user,
};
bootstrapContext.getController = createGetController(bootstrapContext);
return bootstrapContext;