Skip to content

Commit

Permalink
Merge branch 'main' into remove-empty-cat-filter
Browse files Browse the repository at this point in the history
  • Loading branch information
dbauszus-glx committed Mar 22, 2024
2 parents 8feddbb + aed8af6 commit 3c7c029
Show file tree
Hide file tree
Showing 10 changed files with 298 additions and 118 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/unit_tests.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Jest unit tests
name: Codi unit tests

on:
push:
Expand Down
26 changes: 0 additions & 26 deletions lib/layer/decorate.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -179,32 +179,6 @@ export default async function decorate(layer) {
layer.style.label = typeof layer.style.label === 'object' ? layer.style.label : layer.style.labels[layer.style.label || Object.keys(layer.style.labels)[0]];
}

// Handle role-based configurations.
if (Array.isArray(mapp.user?.roles)) {
for (const role in layer.roles || {}) {
if (layer.roles[role] !== null && typeof layer.roles[role] === 'object') {
const negatedRole = role.match(/(?<=^!)(.*)/g)?.[0];
if (mapp.user.roles.includes(role)) {
mapp.utils.merge(layer, layer.roles[role]);
} else if (negatedRole && !mapp.user.roles.includes(negatedRole)) {
mapp.utils.merge(layer, layer.roles[role]);
}
}
}
layer.infoj?.filter(entry => typeof entry.roles === 'object').forEach(entry => {
for (const role in entry.roles) {
if (typeof entry.roles[role] === 'object') {
const roleName = role.match(/(?<=^!)(.*)/g)?.[0];
if (mapp.user.roles.includes(role)) {
mapp.utils.merge(entry, entry.roles[role]);
} else if (roleName && !mapp.user.roles.includes(roleName)) {
mapp.utils.merge(entry, entry.roles[role]);
}
}
}
});
}

// Call layer and/or plugin methods.
Object.keys(layer).forEach((key) => {
typeof mapp.layer[key] === 'function' && mapp.layer[key]?.(layer);
Expand Down
17 changes: 5 additions & 12 deletions mod/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,9 +168,7 @@ module.exports = async (req, res) => {
// Change value may only contain a limited set of whitelisted characters.
if (!reserved.has(param) && !/^[A-Za-z0-9,"'._-\s]*$/.test(change)) {

// Err and return empty string if the change value is invalid.
console.error('Change param no bueno')
return ''
throw new Error(`Substitute \${${param}} value rejected: ${change}`);
}

return change
Expand Down Expand Up @@ -302,16 +300,11 @@ async function layerQuery(req, res) {
// Layer queries must have a geom param.
req.params.geom ??= req.params.layer.geom

// Get array of role filter from layer configuration.
const roles = Roles.filter(req.params.layer, req.params.user?.roles)

// Create params filter string from roleFilter filter params.
req.params.filter = `
${req.params.layer.filter?.default && `AND ${sqlFilter(req.params.layer.filter.default, req.params.SQL)}` || ''}
${req.params.filter && `AND ${sqlFilter(JSON.parse(req.params.filter), req.params.SQL)}` || ''}
${roles && Object.values(roles).some(r => !!r)
? `AND ${sqlFilter(Object.values(roles).filter(r => !!r), req.params.SQL)}`
: ''}`
req.params.filter = [
req.params.layer.filter?.default && `AND ${sqlFilter(req.params.layer.filter.default, req.params.SQL)}` || '',
req.params.filter && `AND ${sqlFilter(JSON.parse(req.params.filter), req.params.SQL)}` || '']
.join(' ')

if (req.params.viewport) {

Expand Down
92 changes: 59 additions & 33 deletions mod/utils/roles.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
@module /utils/roles
*/

const merge = require('./merge')

module.exports = {
check,
filter,
get
get,
objMerge
}

function check(obj, user_roles) {
Expand All @@ -14,10 +16,9 @@ function check(obj, user_roles) {
if (!obj.roles) return obj;

// Always return object with '*' asterisk role.
if (Object.hasOwn(obj.roles,'*')) return obj;
if (Object.hasOwn(obj.roles, '*')) return obj;

// There are no user roles or user_roles are not an array.
if (!user_roles || !Array.isArray(user_roles)) return false;
if (user_roles === undefined) return false

// Some negated role is included in user_roles[]
const someNegatedRole = Object.keys(obj.roles).some(
Expand All @@ -44,34 +45,6 @@ function check(obj, user_roles) {
return false;
}

// Return an object with filter matching the layer.roles with user_roles.
function filter(layer, user_roles) {

// The layer must have roles.
if (!layer.roles) return;

// user_roles must be an array.
if (!Array.isArray(user_roles)) return;

const roleFilter = Object.keys(layer.roles)

// filter roles with a filter object.
.filter(key => layer.roles[key] && typeof layer.roles[key].filter === 'object')

// filter roles included in the user_roles array.
.filter(key => user_roles.includes(key)

// or negated roles (!) NOT included in the array.
|| !user_roles.includes(key.match(/(?<=^!)(.*)/g)?.[0]))

.reduce((o, key) => {
o[key] = layer.roles[key].filter
return o
}, {})

return roleFilter
}

function get(obj) {

const roles = new Set();
Expand All @@ -98,4 +71,57 @@ function get(obj) {
roles.delete('*')

return Array.from(roles)
}

function objMerge(obj, user_roles) {

if (typeof obj !== 'object') return obj;

if (user_roles === undefined) return obj

if (Array.isArray(obj)) {

return obj.map(arrEntry => objMerge(arrEntry, user_roles))
}

Object.keys(obj)
.filter(key => typeof obj[key] === 'object')
.forEach(key => {

// Cannot convert undefined or null to object.
if (!obj[key]) return;

obj[key] = objMerge(obj[key], user_roles)
})

if (!obj.roles) return obj;

if (typeof obj.roles !== 'object') return obj;

if (Array.isArray(obj.roles)) return obj;

if (typeof obj.roles === 'function') return obj;

const clone = structuredClone(obj)

function notIncludesNegatedRole(role, user_roles) {

return role.match(/(?<=^!)(.*)/g)?.[0]?
!user_roles.includes(role.match(/(?<=^!)(.*)/g)?.[0]):
false
}

Object.keys(clone.roles)
.filter(role => clone.roles[role] !== true)
.filter(role => clone.roles[role] !== null)
.filter(role => typeof clone.roles[role] === 'object')
.filter(role => !Array.isArray(clone.roles[role]))
.filter(role => user_roles.includes(role) || notIncludesNegatedRole(role, user_roles))
.forEach(role => {
merge(clone, clone.roles[role])
})

delete clone.roles

return clone
}
38 changes: 23 additions & 15 deletions mod/workspace/_workspace.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ async function layer(req, res) {
const locale = workspace.locales[req.params.locale]

const roles = req.params.user?.roles || []

roles.push('*')

if (!Roles.check(locale, roles)) {
return res.status(403).send('Role access denied for locale.')
Expand All @@ -54,21 +56,21 @@ async function layer(req, res) {
return res.status(400).send(`Unable to validate layer param.`)
}

const layer = await getLayer(req.params)
let layer = await getLayer(req.params)

if (!Roles.check(layer, roles)) {
return res.status(403).send('Role access denied for layer.')
}

layer = Roles.objMerge(layer, roles)

res.json(layer)
}

function locales(req, res) {

const roles = req.params.user?.roles || []

const locales = Object.values(workspace.locales)
.filter(locale => !!Roles.check(locale, roles))
.filter(locale => !!Roles.check(locale, req.params.user?.roles))
.map(locale => ({
key: locale.key,
name: locale.name
Expand Down Expand Up @@ -104,28 +106,34 @@ async function locale(req, res) {
matched => process.env[`SRC_${matched.replace(/(^\${)|(}$)/g, '')}`])
)

const roles = req.params.user?.roles || []

roles.push('*')

// Return layer object instead of array of layer keys
if (req.params.layers) {

const layers = []

for (const key of Object.keys(locale.layers)) {

const layer = await getLayer({
const layers = Object.keys(locale.layers)
.map(async key => await getLayer({
...req.params,
layer: key
})
}))

if (layer instanceof Error) continue;
if(!Roles.check(layer, req.params.user?.roles)) continue;
await Promise.all(layers).then(layers=>{

layers.push(layer)
}
locale.layers = layers
.filter(layer => !!layer)
.filter(layer => !(layer instanceof Error))
.filter(layer => Roles.check(layer, roles))
})

locale.layers = layers
// Also merges roles in layer objects.
locale = Roles.objMerge(locale, roles)

return res.json(locale)
}

locale = Roles.objMerge(locale, roles)

// Check layer access.
locale.layers = locale.layers && Object.entries(locale.layers)
Expand Down
3 changes: 3 additions & 0 deletions mod/workspace/getLayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ module.exports = async (params) => {

let layer = locale.layers[params.layer]

// layer maybe null or undefined.
if (!layer) return;

// Return already merged layer.
if (layer.merged) return layer

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"node": ">=18.x"
},
"scripts": {
"test": "jest",
"test": "codi tests",
"_build": "npx esbuild ./lib/mapp.mjs ./lib/ui.mjs --bundle --minify --tree-shaking=false --sourcemap --format=iife --outdir=./public/js/lib",
"version": "node version.js",
"generate-docs-mapp": "jsdoc --configure jsdoc_mapp.json --verbose",
Expand All @@ -29,6 +29,7 @@
},
"devDependencies": {
"clean-jsdoc-theme": "^4.2.17",
"codi-test-framework": "^0.0.10",
"cookie-parser": "^1.4.5",
"dotenv": "^16.4.5",
"esbuild": "^0.19.11",
Expand All @@ -38,7 +39,6 @@
"eslint-plugin-n": "^15.6.0",
"eslint-plugin-promise": "^6.1.1",
"express": "^4.18.3",
"jest": "^29.5.0",
"jsdocs": "^0.0.1",
"uhtml": "^3.1.0"
}
Expand Down
81 changes: 81 additions & 0 deletions tests/mod/utils/merge.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { describe, it, assertEqual } from 'codi-test-framework';
import mergeDeep from '../../../mod/utils/merge.js'

describe('mergeDeep Module', () => {

it('should merge objects deeply', () => {
const target = {
name: 'Rob',
age: 28,
address: {
street: '6 fourteenth street',
city: 'Johannesburg',
},
hobbies: ['squash', 'guitar'],
};

const source = {
name: 'Rob',
age: 28,
address: {
street: '6 fourteenth street',
city: 'Johannesburg',
},
hobbies: ['cooking'],
};

const expected = {
name: 'Rob',
age: 28,
address: {
street: '6 fourteenth street',
city: 'Johannesburg',
},
hobbies: ['squash', 'guitar', 'cooking'],
};

const mergedObj = mergeDeep(target, source);

assertEqual(mergedObj, expected)
});

it('should merge arrays deeply', () => {
const target = {
fruits: ['apple', 'banana'],
};

const source = {
fruits: ['banana', 'orange'],
};

const expected = {
fruits: ['apple', 'banana','banana', 'orange'],
};

const mergedObj = mergeDeep(target, source);

assertEqual(mergedObj, expected)
});

it('should handle merging with null or undefined values', () => {
const target = {
name: 'John',
age: 30,
};

const source1 = null;
const source2 = undefined;

const expected = {
name: 'John',
age: 30,
};

const mergedObj1 = mergeDeep(target, source1);
const mergedObj2 = mergeDeep(target, source2);

assertEqual(mergedObj1, expected);
assertEqual(mergedObj2, expected);
});

});
Loading

0 comments on commit 3c7c029

Please sign in to comment.