mirror of
https://github.com/directus/directus.git
synced 2026-01-29 21:28:02 -05:00
Merge pull request #62 from directus/permissions
Add permissions + start on CLI
This commit is contained in:
980
package-lock.json
generated
980
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
93
package.json
93
package.json
@@ -1,12 +1,16 @@
|
||||
{
|
||||
"name": "api-node",
|
||||
"version": "0.0.1",
|
||||
"version": "9.0.0-alpha.3",
|
||||
"description": "The Directus API in Node.js",
|
||||
"main": "dist/server.js",
|
||||
"bin": {
|
||||
"directus": "dist/cli/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "NODE_ENV=production node dist/server.js",
|
||||
"build": "rimraf dist && tsc -b && copyfiles \"src/**/*.*\" -e \"src/**/*.ts\" -u 1 dist",
|
||||
"dev": "LOG_LEVEL=trace ts-node-dev src/server.ts --clear --watch \"src/**/*.ts\" --rs --transpile-only | pino-colada"
|
||||
"dev": "LOG_LEVEL=trace ts-node-dev --files src/server.ts --clear --watch \"src/**/*.ts\" --rs --transpile-only | pino-colada",
|
||||
"cli": "ts-node --script-mode --transpile-only src/cli/index.ts"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -27,14 +31,60 @@
|
||||
"url": "https://github.com/directus/api-node/issues"
|
||||
},
|
||||
"homepage": "https://github.com/directus/api-node#readme",
|
||||
"dependencies": {
|
||||
"@hapi/joi": "^17.1.1",
|
||||
"@slynova/flydrive": "^1.0.2",
|
||||
"@slynova/flydrive-gcs": "^1.0.2",
|
||||
"@slynova/flydrive-s3": "^1.0.2",
|
||||
"argon2": "^0.26.2",
|
||||
"atob": "^2.1.2",
|
||||
"body-parser": "^1.19.0",
|
||||
"busboy": "^0.3.1",
|
||||
"camelcase": "^6.0.0",
|
||||
"chalk": "^4.1.0",
|
||||
"clear": "^0.1.0",
|
||||
"commander": "^5.1.0",
|
||||
"cookie-parser": "^1.4.5",
|
||||
"dotenv": "^8.2.0",
|
||||
"exif-reader": "^1.0.3",
|
||||
"express": "^4.17.1",
|
||||
"express-async-handler": "^1.1.4",
|
||||
"express-pino-logger": "^5.0.0",
|
||||
"express-session": "^1.17.1",
|
||||
"fs-extra": "^9.0.1",
|
||||
"get-port": "^5.1.1",
|
||||
"grant": "^5.2.0",
|
||||
"icc": "^2.0.0",
|
||||
"inquirer": "^7.3.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"knex": "^0.21.1",
|
||||
"liquidjs": "^9.14.1",
|
||||
"lodash": "^4.17.19",
|
||||
"ms": "^2.1.2",
|
||||
"mssql": "^6.2.0",
|
||||
"mysql": "^2.18.1",
|
||||
"nanoid": "^3.1.10",
|
||||
"nodemailer": "^6.4.10",
|
||||
"oracledb": "^5.0.0",
|
||||
"pg": "^8.3.0",
|
||||
"pino": "^6.3.2",
|
||||
"resolve-cwd": "^3.0.0",
|
||||
"sharp": "^0.25.4",
|
||||
"sqlite3": "^5.0.0",
|
||||
"uuid": "^8.2.0",
|
||||
"uuid-validate": "0.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/atob": "^2.1.2",
|
||||
"@types/busboy": "^0.2.3",
|
||||
"@types/clear": "^0.1.0",
|
||||
"@types/cookie-parser": "^1.4.2",
|
||||
"@types/express": "^4.17.7",
|
||||
"@types/express-pino-logger": "^4.0.2",
|
||||
"@types/express-session": "^1.17.0",
|
||||
"@types/fs-extra": "^9.0.1",
|
||||
"@types/hapi__joi": "^17.1.3",
|
||||
"@types/inquirer": "^6.5.0",
|
||||
"@types/jsonwebtoken": "^8.5.0",
|
||||
"@types/lodash": "^4.14.157",
|
||||
"@types/ms": "^0.7.31",
|
||||
@@ -52,6 +102,7 @@
|
||||
"prettier": "^2.0.5",
|
||||
"rimraf": "^3.0.2",
|
||||
"ts-node": "^8.10.2",
|
||||
"ts-node-dev": "^1.0.0-pre.51",
|
||||
"tslint": "^6.1.2",
|
||||
"typescript": "^3.9.6"
|
||||
},
|
||||
@@ -64,43 +115,5 @@
|
||||
"*.{js,ts}": [
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@hapi/joi": "^17.1.1",
|
||||
"@slynova/flydrive": "^1.0.2",
|
||||
"@slynova/flydrive-gcs": "^1.0.2",
|
||||
"@slynova/flydrive-s3": "^1.0.2",
|
||||
"argon2": "^0.26.2",
|
||||
"atob": "^2.1.2",
|
||||
"body-parser": "^1.19.0",
|
||||
"busboy": "^0.3.1",
|
||||
"camelcase": "^6.0.0",
|
||||
"cookie-parser": "^1.4.5",
|
||||
"dotenv": "^8.2.0",
|
||||
"exif-reader": "^1.0.3",
|
||||
"express": "^4.17.1",
|
||||
"express-async-handler": "^1.1.4",
|
||||
"express-pino-logger": "^5.0.0",
|
||||
"express-session": "^1.17.1",
|
||||
"get-port": "^5.1.1",
|
||||
"grant": "^5.2.0",
|
||||
"icc": "^2.0.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"knex": "^0.21.1",
|
||||
"liquidjs": "^9.14.1",
|
||||
"lodash": "^4.17.19",
|
||||
"ms": "^2.1.2",
|
||||
"mssql": "^6.2.0",
|
||||
"mysql": "^2.18.1",
|
||||
"nanoid": "^3.1.10",
|
||||
"nodemailer": "^6.4.10",
|
||||
"oracledb": "^5.0.0",
|
||||
"pg": "^8.3.0",
|
||||
"pino": "^6.3.2",
|
||||
"sharp": "^0.25.4",
|
||||
"sqlite3": "^5.0.0",
|
||||
"ts-node-dev": "^1.0.0-pre.51",
|
||||
"uuid": "^8.2.0",
|
||||
"uuid-validate": "0.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,10 @@ const app = express()
|
||||
.use(logger())
|
||||
.use(bodyParser.json())
|
||||
.use(extractToken)
|
||||
.use((req, res, next) => {
|
||||
res.setHeader('X-Powered-By', 'Directus');
|
||||
next();
|
||||
})
|
||||
|
||||
// the auth endpoints allow you to login/logout etc. It should ignore the authentication check
|
||||
.use('/auth', authRouter)
|
||||
|
||||
13
src/cli/create/drivers.ts
Normal file
13
src/cli/create/drivers.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export const drivers = {
|
||||
sqlite3: 'SQLite',
|
||||
mysql: 'MySQL (/ MariaDB / Aurora)',
|
||||
pg: 'PostgreSQL (/ Amazon Redshift)',
|
||||
oracledb: 'Oracle Database',
|
||||
mssql: 'Microsoft SQL Server',
|
||||
};
|
||||
|
||||
export function getDriverForClient(client: string) {
|
||||
for (const [key, value] of Object.entries(drivers)) {
|
||||
if (value === client) return key;
|
||||
}
|
||||
}
|
||||
66
src/cli/create/index.ts
Normal file
66
src/cli/create/index.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import fse from 'fs-extra';
|
||||
import chalk from 'chalk';
|
||||
import inquirer from 'inquirer';
|
||||
import { resolve } from 'path';
|
||||
import { databaseQuestions } from './questions';
|
||||
import { drivers, getDriverForClient } from './drivers';
|
||||
|
||||
export default async function create(directory: string, options: Record<string, any>) {
|
||||
const path = resolve(directory);
|
||||
checkRequirements();
|
||||
|
||||
if (await fse.pathExists(path)) {
|
||||
const stat = await fse.stat(path);
|
||||
|
||||
if (stat.isDirectory() === false) {
|
||||
console.error(
|
||||
`Destination '${chalk.red(directory)}' already exists and is not a directory.`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const files = await fse.readdir(path);
|
||||
|
||||
if (files.length > 0) {
|
||||
console.error(
|
||||
`Destination '${chalk.red(
|
||||
directory
|
||||
)}' already exists and is not an empty directory.`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
let { client } = await inquirer.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'client',
|
||||
message: 'Choose your database client',
|
||||
choices: Object.values(drivers),
|
||||
},
|
||||
]);
|
||||
|
||||
client = getDriverForClient(client);
|
||||
|
||||
const responses = await inquirer.prompt(
|
||||
databaseQuestions[client].map((question) => question({ client }))
|
||||
);
|
||||
|
||||
/** @todo
|
||||
* - See if you can connect to DB
|
||||
* - Install Directus system stuff into DB
|
||||
* - Start the Node API
|
||||
*/
|
||||
}
|
||||
|
||||
function checkRequirements() {
|
||||
const nodeVersion = process.versions.node;
|
||||
const major = +nodeVersion.split('.')[0];
|
||||
|
||||
if (major < 12) {
|
||||
console.error(`You are running Node ${nodeVersion}.`);
|
||||
console.error('Directus requires Node 12 and up.');
|
||||
console.error('Please update your Node version and try again.');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
50
src/cli/create/questions.ts
Normal file
50
src/cli/create/questions.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
const filename = () => ({
|
||||
type: 'input',
|
||||
name: 'filename',
|
||||
message: 'Filename:',
|
||||
default: './data.db',
|
||||
});
|
||||
|
||||
const host = () => ({
|
||||
type: 'input',
|
||||
name: 'host',
|
||||
message: 'Host:',
|
||||
default: '127.0.0.1',
|
||||
});
|
||||
|
||||
const port = ({ client }) => ({
|
||||
type: 'input',
|
||||
name: 'port',
|
||||
message: 'Port:',
|
||||
default() {
|
||||
const ports = {
|
||||
pg: 5432,
|
||||
mysql: 3306,
|
||||
oracledb: 1521,
|
||||
mssql: 1433,
|
||||
};
|
||||
|
||||
return ports[client];
|
||||
},
|
||||
});
|
||||
|
||||
const username = () => ({
|
||||
type: 'input',
|
||||
name: 'username',
|
||||
message: 'Username:',
|
||||
});
|
||||
|
||||
const password = () => ({
|
||||
type: 'password',
|
||||
name: 'password',
|
||||
message: 'Password:',
|
||||
mask: '*',
|
||||
});
|
||||
|
||||
export const databaseQuestions = {
|
||||
sqlite3: [filename],
|
||||
mysql: [host, port, username, password],
|
||||
pg: [host, port, username, password],
|
||||
oracledb: [host, port, username, password],
|
||||
mssql: [host, port, username, password],
|
||||
};
|
||||
18
src/cli/index.ts
Normal file
18
src/cli/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import program from 'commander';
|
||||
|
||||
const pkg = require('../../package.json');
|
||||
|
||||
import start from './start';
|
||||
import create from './create';
|
||||
|
||||
program.version(pkg.version, '-v, --version');
|
||||
|
||||
program.name('directus').usage('[command] [options]');
|
||||
|
||||
program.command('create <directory>').description('Create a new Directus Project').action(create);
|
||||
|
||||
program.command('start').description('Start the Directus API').action(start);
|
||||
|
||||
program.parseAsync(process.argv);
|
||||
14
src/cli/start.ts
Normal file
14
src/cli/start.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import app from '../app';
|
||||
import logger from '../logger';
|
||||
import getPort from 'get-port';
|
||||
import clear from 'clear';
|
||||
|
||||
export default async function start() {
|
||||
clear();
|
||||
|
||||
const port = process.env.PORT || (await getPort({ port: 3000 }));
|
||||
|
||||
app.listen(port, () => {
|
||||
logger.info(`Server started at port ${port}`);
|
||||
});
|
||||
}
|
||||
@@ -1,13 +1,10 @@
|
||||
import knex from 'knex';
|
||||
import logger from '../logger';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
import SchemaInspector from '../knex-schema-inspector/lib/index';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const log = logger.child({ module: 'sql' });
|
||||
|
||||
const database = knex({
|
||||
client: process.env.DB_CLIENT,
|
||||
connection: {
|
||||
@@ -27,8 +24,6 @@ const database = knex({
|
||||
},
|
||||
});
|
||||
|
||||
database.on('query', (data) => log.trace(data.sql));
|
||||
|
||||
export const schemaInspector = SchemaInspector(database);
|
||||
|
||||
export default database;
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
import { AST, NestedCollectionAST } from '../types/ast';
|
||||
import { uniq } from 'lodash';
|
||||
import { clone, uniq, pick } from 'lodash';
|
||||
import database, { schemaInspector } from './index';
|
||||
import { Query } from '../types/query';
|
||||
import { Filter, Query } from '../types';
|
||||
import { QueryBuilder } from 'knex';
|
||||
|
||||
export default async function runAST(ast: AST, query = ast.query) {
|
||||
const toplevelFields: string[] = [];
|
||||
const tempFields: string[] = [];
|
||||
const nestedCollections: NestedCollectionAST[] = [];
|
||||
const primaryKeyField = await schemaInspector.primary(ast.name);
|
||||
const columnsInCollection = (await schemaInspector.columns(ast.name)).map(
|
||||
({ column }) => column
|
||||
);
|
||||
|
||||
for (const child of ast.children) {
|
||||
if (child.type === 'field') {
|
||||
toplevelFields.push(child.name);
|
||||
if (columnsInCollection.includes(child.name) || child.name === '*') {
|
||||
toplevelFields.push(child.name);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -23,33 +32,19 @@ export default async function runAST(ast: AST, query = ast.query) {
|
||||
nestedCollections.push(child);
|
||||
}
|
||||
|
||||
const dbQuery = database.select(toplevelFields).from(ast.name);
|
||||
/** Always fetch primary key in case there's a nested relation that needs it */
|
||||
if (toplevelFields.includes(primaryKeyField) === false) {
|
||||
tempFields.push(primaryKeyField);
|
||||
}
|
||||
|
||||
let dbQuery = database.select([...toplevelFields, ...tempFields]).from(ast.name);
|
||||
|
||||
// Query defaults
|
||||
query.limit = query.limit || 100;
|
||||
query.sort = query.sort || [{ column: primaryKeyField, order: 'asc' }];
|
||||
|
||||
if (query.filter) {
|
||||
query.filter.forEach((filter) => {
|
||||
if (filter.operator === 'in') {
|
||||
let value = filter.value;
|
||||
if (typeof value === 'string') value = value.split(',');
|
||||
|
||||
dbQuery.whereIn(filter.column, value as string[]);
|
||||
}
|
||||
|
||||
if (filter.operator === 'eq') {
|
||||
dbQuery.where({ [filter.column]: filter.value });
|
||||
}
|
||||
|
||||
if (filter.operator === 'neq') {
|
||||
dbQuery.whereNot({ [filter.column]: filter.value });
|
||||
}
|
||||
|
||||
if (filter.operator === 'null') {
|
||||
dbQuery.whereNull(filter.column);
|
||||
}
|
||||
|
||||
if (filter.operator === 'nnull') {
|
||||
dbQuery.whereNotNull(filter.column);
|
||||
}
|
||||
});
|
||||
applyFilter(dbQuery, query.filter);
|
||||
}
|
||||
|
||||
if (query.sort) {
|
||||
@@ -96,69 +91,129 @@ export default async function runAST(ast: AST, query = ast.query) {
|
||||
const m2o = isM2O(batch);
|
||||
|
||||
let batchQuery: Query = {};
|
||||
let tempField: string = null;
|
||||
let tempLimit: number = null;
|
||||
|
||||
if (m2o) {
|
||||
// Make sure we always fetch the nested items primary key field to ensure we have the key to match the item by
|
||||
const toplevelFields = batch.children
|
||||
.filter(({ type }) => type === 'field')
|
||||
.map(({ name }) => name);
|
||||
if (toplevelFields.includes(batch.relation.primary_one) === false) {
|
||||
tempField = batch.relation.primary_one;
|
||||
batch.children.push({ type: 'field', name: batch.relation.primary_one });
|
||||
}
|
||||
|
||||
batchQuery = {
|
||||
...batch.query,
|
||||
filter: [
|
||||
...(batch.query.filter || []),
|
||||
{
|
||||
column: 'id',
|
||||
operator: 'in',
|
||||
// filter removes null / undefined
|
||||
value: uniq(results.map((res) => res[batch.relation.field_many])).filter(
|
||||
filter: {
|
||||
...(batch.query.filter || {}),
|
||||
[batch.relation.primary_one]: {
|
||||
_in: uniq(results.map((res) => res[batch.relation.field_many])).filter(
|
||||
(id) => id
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
} else {
|
||||
// o2m
|
||||
// Make sure we always fetch the related m2o field to ensure we have the foreign key to
|
||||
// match the items by
|
||||
const toplevelFields = batch.children
|
||||
.filter(({ type }) => type === 'field')
|
||||
.map(({ name }) => name);
|
||||
if (toplevelFields.includes(batch.relation.field_many) === false) {
|
||||
tempField = batch.relation.field_many;
|
||||
batch.children.push({ type: 'field', name: batch.relation.field_many });
|
||||
}
|
||||
|
||||
batchQuery = {
|
||||
...batch.query,
|
||||
filter: [
|
||||
...(batch.query.filter || []),
|
||||
{
|
||||
column: batch.relation.field_many,
|
||||
operator: 'in',
|
||||
// filter removes null / undefined
|
||||
value: uniq(results.map((res) => res[batch.parentKey])).filter((id) => id),
|
||||
filter: {
|
||||
...(batch.query.filter || {}),
|
||||
[batch.relation.field_many]: {
|
||||
_in: uniq(results.map((res) => res[batch.parentKey])).filter((id) => id),
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* The nested queries are done with a WHERE m2o IN (pk, pk, pk) query. We have to remove
|
||||
* LIMIT from that equation to ensure we limit `n` items _per parent record_ instead of
|
||||
* `n` items in total. This limit will then be re-applied in the stitching process
|
||||
* down below
|
||||
*/
|
||||
if (batchQuery.limit) {
|
||||
tempLimit = batchQuery.limit;
|
||||
delete batchQuery.limit;
|
||||
}
|
||||
}
|
||||
|
||||
const nestedResults = await runAST(batch, batchQuery);
|
||||
|
||||
results = results.map((record) => {
|
||||
if (m2o) {
|
||||
return {
|
||||
...record,
|
||||
[batch.fieldKey]:
|
||||
const nestedResult =
|
||||
clone(
|
||||
nestedResults.find((nestedRecord) => {
|
||||
return (
|
||||
nestedRecord[batch.relation.primary_one] === record[batch.fieldKey]
|
||||
);
|
||||
}) || null,
|
||||
})
|
||||
) || null;
|
||||
|
||||
if (tempField && nestedResult) {
|
||||
delete nestedResult[tempField];
|
||||
}
|
||||
|
||||
return {
|
||||
...record,
|
||||
[batch.fieldKey]: nestedResult,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...record,
|
||||
[batch.fieldKey]: nestedResults.filter((nestedRecord) => {
|
||||
/**
|
||||
* @todo
|
||||
* pull the name ID from somewhere real
|
||||
*/
|
||||
// o2m
|
||||
let resultsForCurrentRecord = nestedResults
|
||||
.filter((nestedRecord) => {
|
||||
return (
|
||||
nestedRecord[batch.relation.field_many] === record.id ||
|
||||
nestedRecord[batch.relation.field_many]?.id === record.id
|
||||
nestedRecord[batch.relation.field_many] ===
|
||||
record[batch.relation.primary_one] ||
|
||||
// In case of nested object:
|
||||
nestedRecord[batch.relation.field_many]?.[batch.relation.primary_many] ===
|
||||
record[batch.relation.primary_one]
|
||||
);
|
||||
}),
|
||||
})
|
||||
.map((nestedRecord) => {
|
||||
if (tempField) {
|
||||
delete nestedRecord[tempField];
|
||||
}
|
||||
|
||||
return nestedRecord;
|
||||
});
|
||||
|
||||
// Reapply LIMIT query on a per-record basis
|
||||
if (tempLimit) {
|
||||
resultsForCurrentRecord = resultsForCurrentRecord.slice(0, tempLimit);
|
||||
}
|
||||
|
||||
const newRecord = {
|
||||
...record,
|
||||
[batch.fieldKey]: resultsForCurrentRecord,
|
||||
};
|
||||
|
||||
return newRecord;
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
const nestedCollectionKeys = nestedCollections.map(({ fieldKey }) => fieldKey);
|
||||
|
||||
if (toplevelFields.includes('*')) {
|
||||
return results;
|
||||
}
|
||||
|
||||
return results.map((result) =>
|
||||
pick(result, uniq([...nestedCollectionKeys, ...toplevelFields]))
|
||||
);
|
||||
}
|
||||
|
||||
function isM2O(child: NestedCollectionAST) {
|
||||
@@ -166,3 +221,48 @@ function isM2O(child: NestedCollectionAST) {
|
||||
child.relation.collection_one === child.name && child.relation.field_many === child.fieldKey
|
||||
);
|
||||
}
|
||||
|
||||
function applyFilter(dbQuery: QueryBuilder, filter: Filter) {
|
||||
for (const [key, value] of Object.entries(filter)) {
|
||||
if (key.startsWith('_') === false) {
|
||||
let operator = Object.keys(value)[0];
|
||||
|
||||
const compareValue = Object.values(value)[0];
|
||||
|
||||
if (operator === '_eq') {
|
||||
dbQuery.where({ [key]: compareValue });
|
||||
}
|
||||
|
||||
if (operator === '_neq') {
|
||||
dbQuery.whereNot({ [key]: compareValue });
|
||||
}
|
||||
|
||||
if (operator === '_in') {
|
||||
let value = compareValue;
|
||||
if (typeof value === 'string') value = value.split(',');
|
||||
|
||||
dbQuery.whereIn(key, value as string[]);
|
||||
}
|
||||
|
||||
if (operator === '_null') {
|
||||
dbQuery.whereNull(key);
|
||||
}
|
||||
|
||||
if (operator === '_nnull') {
|
||||
dbQuery.whereNotNull(key);
|
||||
}
|
||||
}
|
||||
|
||||
if (key === '_or') {
|
||||
value.forEach((subFilter: Record<string, any>) => {
|
||||
dbQuery.orWhere((subQuery) => applyFilter(subQuery, subFilter));
|
||||
});
|
||||
}
|
||||
|
||||
if (key === '_and') {
|
||||
value.forEach((subFilter: Record<string, any>) => {
|
||||
dbQuery.andWhere((subQuery) => applyFilter(subQuery, subFilter));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
7
src/exceptions/forbidden.ts
Normal file
7
src/exceptions/forbidden.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { BaseException } from './base';
|
||||
|
||||
export class ForbiddenException extends BaseException {
|
||||
constructor(message = `You don't have permission to access this.`) {
|
||||
super(message, 403, 'NO_PERMISSION');
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
export * from './base';
|
||||
export * from './collection-not-found';
|
||||
export * from './field-not-found';
|
||||
export * from './forbidden';
|
||||
export * from './invalid-credentials';
|
||||
export * from './invalid-payload';
|
||||
export * from './invalid-query';
|
||||
|
||||
@@ -39,18 +39,6 @@ if (emailTransport === 'sendmail') {
|
||||
pass: process.env.EMAIL_SMTP_PASSWORD,
|
||||
},
|
||||
} as any);
|
||||
|
||||
logger.trace('[Email] Verifying SMTP connection.');
|
||||
|
||||
transporter
|
||||
.verify()
|
||||
.then(() => {
|
||||
logger.info('[Email] SMTP connected. Ready to send emails.');
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error(`[Email] Couldn't connect to SMTP server:`);
|
||||
logger.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
export type EmailOptions = {
|
||||
|
||||
@@ -9,7 +9,10 @@ import { InvalidCredentialsException } from '../exceptions';
|
||||
* Verify the passed JWT and assign the user ID and role to `req`
|
||||
*/
|
||||
const authenticate: RequestHandler = asyncHandler(async (req, res, next) => {
|
||||
/** @todo base this on a validation middleware on permissions */
|
||||
req.user = null;
|
||||
req.role = null;
|
||||
req.admin = false;
|
||||
|
||||
if (!req.token) return next();
|
||||
|
||||
if (isJWT(req.token)) {
|
||||
@@ -26,14 +29,17 @@ const authenticate: RequestHandler = asyncHandler(async (req, res, next) => {
|
||||
}
|
||||
|
||||
const user = await database
|
||||
.select('role')
|
||||
.select('role', 'directus_roles.admin')
|
||||
.from('directus_users')
|
||||
.where({ id: payload.id })
|
||||
.leftJoin('directus_roles', 'directus_users.role', 'directus_roles.id')
|
||||
.where({ 'directus_users.id': payload.id })
|
||||
.first();
|
||||
|
||||
/** @TODO verify user status */
|
||||
|
||||
req.user = payload.id;
|
||||
req.role = user.role;
|
||||
req.admin = user.admin;
|
||||
return next();
|
||||
}
|
||||
|
||||
@@ -41,6 +47,7 @@ const authenticate: RequestHandler = asyncHandler(async (req, res, next) => {
|
||||
* @TODO
|
||||
* Implement static tokens
|
||||
*
|
||||
* @NOTE
|
||||
* We'll silently ignore wrong tokens. This makes sure we prevent brute-forcing static tokens
|
||||
*/
|
||||
return next();
|
||||
|
||||
@@ -5,15 +5,15 @@
|
||||
import { RequestHandler } from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import database from '../database';
|
||||
import { CollectionNotFoundException } from '../exceptions';
|
||||
import { ForbiddenException } from '../exceptions';
|
||||
|
||||
const validateCollection: RequestHandler = asyncHandler(async (req, res, next) => {
|
||||
const collectionExists: RequestHandler = asyncHandler(async (req, res, next) => {
|
||||
if (!req.params.collection) return next();
|
||||
|
||||
const exists = await database.schema.hasTable(req.params.collection);
|
||||
|
||||
if (exists === false) {
|
||||
throw new CollectionNotFoundException(req.params.collection);
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
req.collection = req.params.collection;
|
||||
@@ -23,9 +23,10 @@ const validateCollection: RequestHandler = asyncHandler(async (req, res, next) =
|
||||
.from('directus_collections')
|
||||
.where({ collection: req.collection })
|
||||
.first();
|
||||
|
||||
req.single = single;
|
||||
|
||||
return next();
|
||||
});
|
||||
|
||||
export default validateCollection;
|
||||
export default collectionExists;
|
||||
@@ -4,16 +4,19 @@
|
||||
*/
|
||||
|
||||
import { RequestHandler } from 'express';
|
||||
import { Query, Sort, Filter, FilterOperator } from '../types/query';
|
||||
import { Query, Sort, Filter } from '../types/query';
|
||||
import { Meta } from '../types/meta';
|
||||
import logger from '../logger';
|
||||
|
||||
const sanitizeQuery: RequestHandler = (req, res, next) => {
|
||||
if (!req.query) return;
|
||||
|
||||
const query: Query = {};
|
||||
const query: Query = {
|
||||
fields: sanitizeFields(req.query.fields) || ['*'],
|
||||
};
|
||||
|
||||
if (req.query.fields) {
|
||||
query.fields = sanitizeFields(req.query.fields);
|
||||
if (req.query.limit) {
|
||||
query.limit = sanitizeLimit(req.query.limit);
|
||||
}
|
||||
|
||||
if (req.query.sort) {
|
||||
@@ -24,13 +27,6 @@ const sanitizeQuery: RequestHandler = (req, res, next) => {
|
||||
query.filter = sanitizeFilter(req.query.filter);
|
||||
}
|
||||
|
||||
if (req.query.limit) {
|
||||
query.limit = sanitizeLimit(req.query.limit);
|
||||
} else {
|
||||
/** @todo is this the right place to set these defaults? */
|
||||
query.limit = 100;
|
||||
}
|
||||
|
||||
if (req.query.limit == '-1') {
|
||||
delete query.limit;
|
||||
}
|
||||
@@ -55,6 +51,13 @@ const sanitizeQuery: RequestHandler = (req, res, next) => {
|
||||
query.search = req.query.search;
|
||||
}
|
||||
|
||||
if (req.permissions) {
|
||||
query.filter = {
|
||||
...(query.filter || {}),
|
||||
...(req.permissions.permissions || {}),
|
||||
};
|
||||
}
|
||||
|
||||
req.sanitizedQuery = query;
|
||||
return next();
|
||||
};
|
||||
@@ -62,6 +65,8 @@ const sanitizeQuery: RequestHandler = (req, res, next) => {
|
||||
export default sanitizeQuery;
|
||||
|
||||
function sanitizeFields(rawFields: any) {
|
||||
if (!rawFields) return;
|
||||
|
||||
let fields: string[] = [];
|
||||
|
||||
if (typeof rawFields === 'string') fields = rawFields.split(',');
|
||||
@@ -84,19 +89,26 @@ function sanitizeSort(rawSort: any) {
|
||||
}
|
||||
|
||||
function sanitizeFilter(rawFilter: any) {
|
||||
const filters: Filter[] = [];
|
||||
let filters: Filter = rawFilter;
|
||||
|
||||
Object.keys(rawFilter).forEach((column) => {
|
||||
Object.keys(rawFilter[column]).forEach((operator: FilterOperator) => {
|
||||
const value = rawFilter[column][operator];
|
||||
filters.push({ column, operator, value });
|
||||
});
|
||||
});
|
||||
if (typeof rawFilter === 'string') {
|
||||
try {
|
||||
filters = JSON.parse(rawFilter);
|
||||
} catch {
|
||||
logger.warn('Invalid value passed for filter query parameter.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo
|
||||
* validate filter syntax?
|
||||
*/
|
||||
|
||||
return filters;
|
||||
}
|
||||
|
||||
function sanitizeLimit(rawLimit: any) {
|
||||
if (!rawLimit) return null;
|
||||
return Number(rawLimit);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
/**
|
||||
* Validates query parameters.
|
||||
* We'll check if all fields you're trying to access exist
|
||||
*
|
||||
* This has to be run after sanitizeQuery
|
||||
*/
|
||||
|
||||
import { RequestHandler } from 'express';
|
||||
import { Query } from '../types/query';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import { InvalidQueryException } from '../exceptions';
|
||||
import hasFields from '../utils/has-fields';
|
||||
|
||||
const validateQuery: RequestHandler = asyncHandler(async (req, res, next) => {
|
||||
if (!req.collection) return next();
|
||||
if (!req.sanitizedQuery) return next();
|
||||
|
||||
const query: Query = req.sanitizedQuery;
|
||||
|
||||
await Promise.all([
|
||||
validateParams(req.collection, query),
|
||||
validateFields(req.collection, query),
|
||||
validateMeta(query),
|
||||
]);
|
||||
|
||||
return next();
|
||||
});
|
||||
|
||||
async function validateParams(collection: string, query: Query) {
|
||||
if (query.offset && query.page) {
|
||||
throw new InvalidQueryException(
|
||||
`You can't have both the offset and page query parameters enabled.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function validateFields(collection: string, query: Query) {
|
||||
/**
|
||||
* @todo combine this with permissions (?)
|
||||
*/
|
||||
/**
|
||||
* @todo use /utils/has-fields
|
||||
*/
|
||||
// const fieldsToCheck = new Set<string>();
|
||||
// if (query.fields) {
|
||||
// /** @todo support relationships in here */
|
||||
// query.fields.forEach((field) => fieldsToCheck.add(field));
|
||||
// }
|
||||
// if (query.sort) {
|
||||
// query.sort.forEach((sort) => fieldsToCheck.add(sort.column));
|
||||
// }
|
||||
// /** @todo swap with more efficient schemaInspector version */
|
||||
// const fieldsExist = await hasFields(collection, Array.from(fieldsToCheck));
|
||||
// Array.from(fieldsToCheck).forEach((field, index) => {
|
||||
// const exists = fieldsExist[index];
|
||||
// if (exists === false)
|
||||
// throw new InvalidQueryException(`Field ${field} doesn't exist in ${collection}.`);
|
||||
// });
|
||||
}
|
||||
|
||||
async function validateMeta(query: Query) {
|
||||
if (!query.meta) return;
|
||||
|
||||
return query.meta.every((metaField) => []);
|
||||
}
|
||||
|
||||
export default validateQuery;
|
||||
@@ -1,7 +1,6 @@
|
||||
import express from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import sanitizeQuery from '../middleware/sanitize-query';
|
||||
import validateQuery from '../middleware/validate-query';
|
||||
import * as ActivityService from '../services/activity';
|
||||
import useCollection from '../middleware/use-collection';
|
||||
|
||||
@@ -11,9 +10,12 @@ router.get(
|
||||
'/',
|
||||
useCollection('directus_activity'),
|
||||
sanitizeQuery,
|
||||
validateQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
const records = await ActivityService.readActivities(req.sanitizedQuery);
|
||||
const records = await ActivityService.readActivities(req.sanitizedQuery, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
});
|
||||
|
||||
return res.json({
|
||||
data: records || null,
|
||||
});
|
||||
@@ -24,9 +26,11 @@ router.get(
|
||||
'/:pk',
|
||||
useCollection('directus_activity'),
|
||||
sanitizeQuery,
|
||||
validateQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
const record = await ActivityService.readActivity(req.params.pk, req.sanitizedQuery);
|
||||
const record = await ActivityService.readActivity(req.params.pk, req.sanitizedQuery, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
});
|
||||
|
||||
return res.json({
|
||||
data: record || null,
|
||||
@@ -38,7 +42,6 @@ router.post(
|
||||
'/comment',
|
||||
useCollection('directus_activity'),
|
||||
sanitizeQuery,
|
||||
validateQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
const primaryKey = await ActivityService.createActivity({
|
||||
...req.body,
|
||||
@@ -48,7 +51,10 @@ router.post(
|
||||
user_agent: req.get('user-agent'),
|
||||
});
|
||||
|
||||
const record = await ActivityService.readActivity(primaryKey, req.sanitizedQuery);
|
||||
const record = await ActivityService.readActivity(primaryKey, req.sanitizedQuery, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
});
|
||||
|
||||
return res.json({
|
||||
data: record || null,
|
||||
@@ -60,15 +66,19 @@ router.patch(
|
||||
'/comment/:pk',
|
||||
useCollection('directus_activity'),
|
||||
sanitizeQuery,
|
||||
validateQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
const primaryKey = await ActivityService.updateActivity(req.params.pk, req.body, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
user: req.user,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
});
|
||||
|
||||
const record = await ActivityService.readActivity(primaryKey, req.sanitizedQuery);
|
||||
const record = await ActivityService.readActivity(primaryKey, req.sanitizedQuery, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
});
|
||||
|
||||
return res.json({
|
||||
data: record || null,
|
||||
@@ -81,6 +91,8 @@ router.delete(
|
||||
useCollection('directus_activity'),
|
||||
asyncHandler(async (req, res) => {
|
||||
await ActivityService.deleteActivity(req.params.pk, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
user: req.user,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Router } from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import sanitizeQuery from '../middleware/sanitize-query';
|
||||
import validateQuery from '../middleware/validate-query';
|
||||
import * as CollectionsService from '../services/collections';
|
||||
import database, { schemaInspector } from '../database';
|
||||
import { schemaInspector } from '../database';
|
||||
import { InvalidPayloadException, CollectionNotFoundException } from '../exceptions';
|
||||
import Joi from '@hapi/joi';
|
||||
import useCollection from '../middleware/use-collection';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -25,11 +25,14 @@ const collectionSchema = Joi.object({
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
useCollection('directus_collections'),
|
||||
asyncHandler(async (req, res) => {
|
||||
const { error } = collectionSchema.validate(req.body);
|
||||
if (error) throw new InvalidPayloadException(error.message);
|
||||
|
||||
const createdCollection = await CollectionsService.create(req.body, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
user: req.user,
|
||||
@@ -41,18 +44,21 @@ router.post(
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
useCollection('directus_collections'),
|
||||
sanitizeQuery,
|
||||
validateQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
const collections = await CollectionsService.readAll(req.sanitizedQuery);
|
||||
const collections = await CollectionsService.readAll(req.sanitizedQuery, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
});
|
||||
res.json({ data: collections || null });
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:collection',
|
||||
useCollection('directus_collections'),
|
||||
sanitizeQuery,
|
||||
validateQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
const exists = await schemaInspector.hasTable(req.params.collection);
|
||||
|
||||
@@ -60,7 +66,8 @@ router.get(
|
||||
|
||||
const collection = await CollectionsService.readOne(
|
||||
req.params.collection,
|
||||
req.sanitizedQuery
|
||||
req.sanitizedQuery,
|
||||
{ role: req.role, admin: req.admin }
|
||||
);
|
||||
res.json({ data: collection || null });
|
||||
})
|
||||
@@ -68,12 +75,15 @@ router.get(
|
||||
|
||||
router.delete(
|
||||
'/:collection',
|
||||
useCollection('directus_collections'),
|
||||
asyncHandler(async (req, res) => {
|
||||
if ((await schemaInspector.hasTable(req.params.collection)) === false) {
|
||||
throw new CollectionNotFoundException(req.params.collection);
|
||||
}
|
||||
|
||||
await CollectionsService.deleteCollection(req.params.collection, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
user: req.user,
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
import { Router } from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import * as FieldsService from '../services/fields';
|
||||
import validateCollection from '../middleware/validate-collection';
|
||||
import validateCollection from '../middleware/collection-exists';
|
||||
import { schemaInspector } from '../database';
|
||||
import { FieldNotFoundException, InvalidPayloadException } from '../exceptions';
|
||||
import Joi from '@hapi/joi';
|
||||
import { Field } from '../types/field';
|
||||
import useCollection from '../middleware/use-collection';
|
||||
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* @TODO
|
||||
*
|
||||
* Add accountability / permissions handling to fields
|
||||
*/
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
useCollection('directus_fields'),
|
||||
asyncHandler(async (req, res) => {
|
||||
const fields = await FieldsService.readAll();
|
||||
return res.json({ data: fields || null });
|
||||
@@ -19,6 +27,7 @@ router.get(
|
||||
|
||||
router.get(
|
||||
'/:collection',
|
||||
useCollection('directus_fields'),
|
||||
validateCollection,
|
||||
asyncHandler(async (req, res) => {
|
||||
const fields = await FieldsService.readAll(req.collection);
|
||||
@@ -28,6 +37,7 @@ router.get(
|
||||
|
||||
router.get(
|
||||
'/:collection/:field',
|
||||
useCollection('directus_fields'),
|
||||
validateCollection,
|
||||
asyncHandler(async (req, res) => {
|
||||
const exists = await schemaInspector.hasColumn(req.collection, req.params.field);
|
||||
@@ -51,6 +61,7 @@ const newFieldSchema = Joi.object({
|
||||
|
||||
router.post(
|
||||
'/:collection',
|
||||
useCollection('directus_fields'),
|
||||
validateCollection,
|
||||
asyncHandler(async (req, res) => {
|
||||
const { error } = newFieldSchema.validate(req.body);
|
||||
@@ -62,6 +73,8 @@ router.post(
|
||||
const field: Partial<Field> = req.body;
|
||||
|
||||
const createdField = await FieldsService.createField(req.collection, field, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
user: req.user,
|
||||
|
||||
@@ -2,7 +2,6 @@ import express from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import Busboy from 'busboy';
|
||||
import sanitizeQuery from '../middleware/sanitize-query';
|
||||
import validateQuery from '../middleware/validate-query';
|
||||
import * as FilesService from '../services/files';
|
||||
import logger from '../logger';
|
||||
import { InvalidPayloadException } from '../exceptions';
|
||||
@@ -10,6 +9,8 @@ import useCollection from '../middleware/use-collection';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(useCollection('directus_files'));
|
||||
|
||||
const multipartHandler = (operation: 'create' | 'update') =>
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const busboy = new Busboy({ headers: req.headers });
|
||||
@@ -55,11 +56,16 @@ const multipartHandler = (operation: 'create' | 'update') =>
|
||||
try {
|
||||
if (operation === 'create') {
|
||||
const pk = await FilesService.createFile(payload, fileStream, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
user: req.user,
|
||||
});
|
||||
const file = await FilesService.readFile(pk, req.sanitizedQuery);
|
||||
const file = await FilesService.readFile(pk, req.sanitizedQuery, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
});
|
||||
|
||||
savedFiles.push(file);
|
||||
} else {
|
||||
@@ -67,13 +73,18 @@ const multipartHandler = (operation: 'create' | 'update') =>
|
||||
req.params.pk,
|
||||
payload,
|
||||
{
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
user: req.user,
|
||||
},
|
||||
fileStream
|
||||
);
|
||||
const file = await FilesService.readFile(pk, req.sanitizedQuery);
|
||||
const file = await FilesService.readFile(pk, req.sanitizedQuery, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
});
|
||||
|
||||
savedFiles.push(file);
|
||||
}
|
||||
@@ -93,33 +104,35 @@ const multipartHandler = (operation: 'create' | 'update') =>
|
||||
return req.pipe(busboy);
|
||||
});
|
||||
|
||||
router.post('/', useCollection('directus_files'), multipartHandler('create'));
|
||||
router.post('/', sanitizeQuery, multipartHandler('create'));
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
useCollection('directus_files'),
|
||||
sanitizeQuery,
|
||||
validateQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
const records = await FilesService.readFiles(req.sanitizedQuery);
|
||||
const records = await FilesService.readFiles(req.sanitizedQuery, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
});
|
||||
return res.json({ data: records || null });
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:pk',
|
||||
useCollection('directus_files'),
|
||||
sanitizeQuery,
|
||||
validateQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
const record = await FilesService.readFile(req.params.pk, req.sanitizedQuery);
|
||||
const record = await FilesService.readFile(req.params.pk, req.sanitizedQuery, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
});
|
||||
return res.json({ data: record || null });
|
||||
})
|
||||
);
|
||||
|
||||
router.patch(
|
||||
'/:pk',
|
||||
useCollection('directus_files'),
|
||||
sanitizeQuery,
|
||||
asyncHandler(async (req, res, next) => {
|
||||
let file: Record<string, any>;
|
||||
|
||||
@@ -127,11 +140,16 @@ router.patch(
|
||||
file = await multipartHandler('update')(req, res, next);
|
||||
} else {
|
||||
const pk = await FilesService.updateFile(req.params.pk, req.body, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
user: req.user,
|
||||
});
|
||||
file = await FilesService.readFile(pk, req.sanitizedQuery);
|
||||
file = await FilesService.readFile(pk, req.sanitizedQuery, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).json({ data: file || null });
|
||||
@@ -140,9 +158,10 @@ router.patch(
|
||||
|
||||
router.delete(
|
||||
'/:pk',
|
||||
useCollection('directus_files'),
|
||||
asyncHandler(async (req, res) => {
|
||||
await FilesService.deleteFile(req.params.pk, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
user: req.user,
|
||||
|
||||
@@ -1,60 +1,72 @@
|
||||
import express from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import sanitizeQuery from '../middleware/sanitize-query';
|
||||
import validateQuery from '../middleware/validate-query';
|
||||
import useCollection from '../middleware/use-collection';
|
||||
import * as FoldersService from '../services/folders';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(useCollection('directus_folders'));
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
useCollection('directus_folders'),
|
||||
asyncHandler(async (req, res) => {
|
||||
const primaryKey = await FoldersService.createFolder(req.body, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
user: req.user,
|
||||
});
|
||||
|
||||
const record = await FoldersService.readFolder(primaryKey, req.sanitizedQuery);
|
||||
const record = await FoldersService.readFolder(primaryKey, req.sanitizedQuery, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
});
|
||||
return res.json({ data: record || null });
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
useCollection('directus_folders'),
|
||||
sanitizeQuery,
|
||||
validateQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
const records = await FoldersService.readFolders(req.sanitizedQuery);
|
||||
const records = await FoldersService.readFolders(req.sanitizedQuery, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
});
|
||||
return res.json({ data: records || null });
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:pk',
|
||||
useCollection('directus_folders'),
|
||||
sanitizeQuery,
|
||||
validateQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
const record = await FoldersService.readFolder(req.params.pk, req.sanitizedQuery);
|
||||
const record = await FoldersService.readFolder(req.params.pk, req.sanitizedQuery, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
});
|
||||
return res.json({ data: record || null });
|
||||
})
|
||||
);
|
||||
|
||||
router.patch(
|
||||
'/:pk',
|
||||
useCollection('directus_folders'),
|
||||
sanitizeQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
const primaryKey = await FoldersService.updateFolder(req.params.pk, req.body, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
user: req.user,
|
||||
});
|
||||
|
||||
const record = await FoldersService.readFolder(primaryKey, req.sanitizedQuery);
|
||||
const record = await FoldersService.readFolder(primaryKey, req.sanitizedQuery, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
});
|
||||
|
||||
return res.json({ data: record || null });
|
||||
})
|
||||
@@ -62,9 +74,10 @@ router.patch(
|
||||
|
||||
router.delete(
|
||||
'/:pk',
|
||||
useCollection('directus_folders'),
|
||||
asyncHandler(async (req, res) => {
|
||||
await FoldersService.deleteFolder(req.params.pk, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
user: req.user,
|
||||
|
||||
@@ -2,8 +2,7 @@ import express from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import * as ItemsService from '../services/items';
|
||||
import sanitizeQuery from '../middleware/sanitize-query';
|
||||
import validateCollection from '../middleware/validate-collection';
|
||||
import validateQuery from '../middleware/validate-query';
|
||||
import collectionExists from '../middleware/collection-exists';
|
||||
import * as MetaService from '../services/meta';
|
||||
import { RouteNotFoundException } from '../exceptions';
|
||||
|
||||
@@ -11,21 +10,25 @@ const router = express.Router();
|
||||
|
||||
router.post(
|
||||
'/:collection',
|
||||
validateCollection,
|
||||
collectionExists,
|
||||
sanitizeQuery,
|
||||
validateQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
if (req.single) {
|
||||
throw new RouteNotFoundException(req.path);
|
||||
}
|
||||
|
||||
const primaryKey = await ItemsService.createItem(req.collection, req.body, {
|
||||
user: req.user,
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
user: req.user,
|
||||
});
|
||||
|
||||
const item = await ItemsService.readItem(req.collection, primaryKey, req.sanitizedQuery);
|
||||
const item = await ItemsService.readItem(req.collection, primaryKey, req.sanitizedQuery, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
});
|
||||
|
||||
res.json({ data: item || null });
|
||||
})
|
||||
@@ -33,14 +36,19 @@ router.post(
|
||||
|
||||
router.get(
|
||||
'/:collection',
|
||||
validateCollection,
|
||||
collectionExists,
|
||||
sanitizeQuery,
|
||||
validateQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
const [records, meta] = await Promise.all([
|
||||
req.single
|
||||
? ItemsService.readSingleton(req.collection, req.sanitizedQuery)
|
||||
: ItemsService.readItems(req.collection, req.sanitizedQuery),
|
||||
? ItemsService.readSingleton(req.collection, req.sanitizedQuery, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
})
|
||||
: ItemsService.readItems(req.collection, req.sanitizedQuery, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
}),
|
||||
MetaService.getMetaForQuery(req.collection, req.sanitizedQuery),
|
||||
]);
|
||||
|
||||
@@ -53,9 +61,8 @@ router.get(
|
||||
|
||||
router.get(
|
||||
'/:collection/:pk',
|
||||
validateCollection,
|
||||
collectionExists,
|
||||
sanitizeQuery,
|
||||
validateQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
if (req.single) {
|
||||
throw new RouteNotFoundException(req.path);
|
||||
@@ -64,7 +71,8 @@ router.get(
|
||||
const record = await ItemsService.readItem(
|
||||
req.collection,
|
||||
req.params.pk,
|
||||
req.sanitizedQuery
|
||||
req.sanitizedQuery,
|
||||
{ role: req.role, admin: req.admin }
|
||||
);
|
||||
|
||||
return res.json({
|
||||
@@ -75,21 +83,25 @@ router.get(
|
||||
|
||||
router.patch(
|
||||
'/:collection',
|
||||
validateCollection,
|
||||
collectionExists,
|
||||
sanitizeQuery,
|
||||
validateQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
if (req.single === false) {
|
||||
throw new RouteNotFoundException(req.path);
|
||||
}
|
||||
|
||||
await ItemsService.upsertSingleton(req.collection, req.body, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
user: req.user,
|
||||
});
|
||||
|
||||
const item = await ItemsService.readSingleton(req.collection, req.sanitizedQuery);
|
||||
const item = await ItemsService.readSingleton(req.collection, req.sanitizedQuery, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
});
|
||||
|
||||
return res.json({ data: item || null });
|
||||
})
|
||||
@@ -97,21 +109,25 @@ router.patch(
|
||||
|
||||
router.patch(
|
||||
'/:collection/:pk',
|
||||
validateCollection,
|
||||
collectionExists,
|
||||
sanitizeQuery,
|
||||
validateQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
if (req.single) {
|
||||
throw new RouteNotFoundException(req.path);
|
||||
}
|
||||
|
||||
const primaryKey = await ItemsService.updateItem(req.collection, req.params.pk, req.body, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
user: req.user,
|
||||
});
|
||||
|
||||
const item = await ItemsService.readItem(req.collection, primaryKey, req.sanitizedQuery);
|
||||
const item = await ItemsService.readItem(req.collection, primaryKey, req.sanitizedQuery, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
});
|
||||
|
||||
return res.json({ data: item || null });
|
||||
})
|
||||
@@ -119,9 +135,11 @@ router.patch(
|
||||
|
||||
router.delete(
|
||||
'/:collection/:pk',
|
||||
validateCollection,
|
||||
collectionExists,
|
||||
asyncHandler(async (req, res) => {
|
||||
await ItemsService.deleteItem(req.collection, req.params.pk, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
user: req.user,
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
import express from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import sanitizeQuery from '../middleware/sanitize-query';
|
||||
import validateQuery from '../middleware/validate-query';
|
||||
import * as PermissionsService from '../services/permissions';
|
||||
import useCollection from '../middleware/use-collection';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(useCollection('directus_permissions'));
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
useCollection('directus_permissions'),
|
||||
asyncHandler(async (req, res) => {
|
||||
const primaryKey = await PermissionsService.createPermission(req.body, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
user: req.user,
|
||||
});
|
||||
|
||||
const item = await PermissionsService.readPermission(primaryKey, req.sanitizedQuery);
|
||||
const item = await PermissionsService.readPermission(primaryKey, req.sanitizedQuery, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
});
|
||||
|
||||
return res.json({ data: item || null });
|
||||
})
|
||||
@@ -25,24 +30,24 @@ router.post(
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
useCollection('directus_permissions'),
|
||||
sanitizeQuery,
|
||||
validateQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
const item = await PermissionsService.readPermissions(req.sanitizedQuery);
|
||||
const item = await PermissionsService.readPermissions(req.sanitizedQuery, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
});
|
||||
return res.json({ data: item || null });
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:pk',
|
||||
useCollection('directus_permissions'),
|
||||
sanitizeQuery,
|
||||
validateQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
const record = await PermissionsService.readPermission(
|
||||
Number(req.params.pk),
|
||||
req.sanitizedQuery
|
||||
req.sanitizedQuery,
|
||||
{ role: req.role, admin: req.admin }
|
||||
);
|
||||
return res.json({ data: record || null });
|
||||
})
|
||||
@@ -50,19 +55,23 @@ router.get(
|
||||
|
||||
router.patch(
|
||||
'/:pk',
|
||||
useCollection('directus_permissions'),
|
||||
asyncHandler(async (req, res) => {
|
||||
const primaryKey = await PermissionsService.updatePermission(
|
||||
Number(req.params.pk),
|
||||
req.body,
|
||||
{
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
user: req.user,
|
||||
}
|
||||
);
|
||||
|
||||
const item = await PermissionsService.readPermission(primaryKey, req.sanitizedQuery);
|
||||
const item = await PermissionsService.readPermission(primaryKey, req.sanitizedQuery, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
});
|
||||
|
||||
return res.json({ data: item || null });
|
||||
})
|
||||
@@ -70,9 +79,10 @@ router.patch(
|
||||
|
||||
router.delete(
|
||||
'/:pk',
|
||||
useCollection('directus_permissions'),
|
||||
asyncHandler(async (req, res) => {
|
||||
await PermissionsService.deletePermission(Number(req.params.pk), {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
user: req.user,
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import express from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import sanitizeQuery from '../middleware/sanitize-query';
|
||||
import validateQuery from '../middleware/validate-query';
|
||||
import * as CollectionPresetsService from '../services/presets';
|
||||
import useCollection from '../middleware/use-collection';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(useCollection('directus_presets'));
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
useCollection('directus_presets'),
|
||||
asyncHandler(async (req, res) => {
|
||||
const primaryKey = await CollectionPresetsService.createCollectionPreset(req.body, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
user: req.user,
|
||||
@@ -19,7 +21,8 @@ router.post(
|
||||
|
||||
const record = await CollectionPresetsService.readCollectionPreset(
|
||||
primaryKey,
|
||||
req.sanitizedQuery
|
||||
req.sanitizedQuery,
|
||||
{ role: req.role, admin: req.admin }
|
||||
);
|
||||
return res.json({ data: record || null });
|
||||
})
|
||||
@@ -27,24 +30,24 @@ router.post(
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
useCollection('directus_presets'),
|
||||
sanitizeQuery,
|
||||
validateQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
const records = await CollectionPresetsService.readCollectionPresets(req.sanitizedQuery);
|
||||
const records = await CollectionPresetsService.readCollectionPresets(req.sanitizedQuery, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
});
|
||||
return res.json({ data: records || null });
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:pk',
|
||||
useCollection('directus_presets'),
|
||||
sanitizeQuery,
|
||||
validateQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
const record = await CollectionPresetsService.readCollectionPreset(
|
||||
req.params.pk,
|
||||
req.sanitizedQuery
|
||||
req.sanitizedQuery,
|
||||
{ role: req.role, admin: req.admin }
|
||||
);
|
||||
return res.json({ data: record || null });
|
||||
})
|
||||
@@ -52,14 +55,14 @@ router.get(
|
||||
|
||||
router.patch(
|
||||
'/:pk',
|
||||
useCollection('directus_presets'),
|
||||
sanitizeQuery,
|
||||
validateQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
const primaryKey = await CollectionPresetsService.updateCollectionPreset(
|
||||
req.params.pk,
|
||||
req.body,
|
||||
{
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
user: req.user,
|
||||
@@ -68,7 +71,8 @@ router.patch(
|
||||
|
||||
const record = await CollectionPresetsService.readCollectionPreset(
|
||||
primaryKey,
|
||||
req.sanitizedQuery
|
||||
req.sanitizedQuery,
|
||||
{ role: req.role, admin: req.admin }
|
||||
);
|
||||
return res.json({ data: record || null });
|
||||
})
|
||||
@@ -76,9 +80,10 @@ router.patch(
|
||||
|
||||
router.delete(
|
||||
'/:pk',
|
||||
useCollection('directus_presets'),
|
||||
asyncHandler(async (req, res) => {
|
||||
await CollectionPresetsService.deleteCollectionPreset(req.params.pk, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
user: req.user,
|
||||
|
||||
@@ -1,71 +1,81 @@
|
||||
import express from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import sanitizeQuery from '../middleware/sanitize-query';
|
||||
import validateQuery from '../middleware/validate-query';
|
||||
import * as RelationsService from '../services/relations';
|
||||
import useCollection from '../middleware/use-collection';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(useCollection('directus_relations'));
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
useCollection('directus_relations'),
|
||||
sanitizeQuery,
|
||||
validateQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
const primaryKey = await RelationsService.createRelation(req.body, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
user: req.user,
|
||||
});
|
||||
const item = await RelationsService.readRelation(primaryKey, req.sanitizedQuery);
|
||||
const item = await RelationsService.readRelation(primaryKey, req.sanitizedQuery, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
});
|
||||
return res.json({ data: item || null });
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
useCollection('directus_relations'),
|
||||
sanitizeQuery,
|
||||
validateQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
const records = await RelationsService.readRelations(req.sanitizedQuery);
|
||||
const records = await RelationsService.readRelations(req.sanitizedQuery, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
});
|
||||
return res.json({ data: records || null });
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:pk',
|
||||
useCollection('directus_relations'),
|
||||
sanitizeQuery,
|
||||
validateQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
const record = await RelationsService.readRelation(req.params.pk, req.sanitizedQuery);
|
||||
const record = await RelationsService.readRelation(req.params.pk, req.sanitizedQuery, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
});
|
||||
return res.json({ data: record || null });
|
||||
})
|
||||
);
|
||||
|
||||
router.patch(
|
||||
'/:pk',
|
||||
useCollection('directus_relations'),
|
||||
sanitizeQuery,
|
||||
validateQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
const primaryKey = await RelationsService.updateRelation(req.params.pk, req.body, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
user: req.user,
|
||||
});
|
||||
const item = await RelationsService.readRelation(primaryKey, req.sanitizedQuery);
|
||||
const item = await RelationsService.readRelation(primaryKey, req.sanitizedQuery, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
});
|
||||
return res.json({ data: item || null });
|
||||
})
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:pk',
|
||||
useCollection('directus_relations'),
|
||||
asyncHandler(async (req, res) => {
|
||||
await RelationsService.deleteRelation(Number(req.params.pk), {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
user: req.user,
|
||||
|
||||
@@ -1,30 +1,33 @@
|
||||
import express from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import sanitizeQuery from '../middleware/sanitize-query';
|
||||
import validateQuery from '../middleware/validate-query';
|
||||
import * as RevisionsService from '../services/revisions';
|
||||
import useCollection from '../middleware/use-collection';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(useCollection('directus_revisions'));
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
useCollection('directus_revisions'),
|
||||
sanitizeQuery,
|
||||
validateQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
const records = await RevisionsService.readRevisions(req.sanitizedQuery);
|
||||
const records = await RevisionsService.readRevisions(req.sanitizedQuery, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
});
|
||||
return res.json({ data: records || null });
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:pk',
|
||||
useCollection('directus_revisions'),
|
||||
sanitizeQuery,
|
||||
validateQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
const record = await RevisionsService.readRevision(req.params.pk, req.sanitizedQuery);
|
||||
const record = await RevisionsService.readRevision(req.params.pk, req.sanitizedQuery, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
});
|
||||
return res.json({ data: record || null });
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1,71 +1,81 @@
|
||||
import express from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import sanitizeQuery from '../middleware/sanitize-query';
|
||||
import validateQuery from '../middleware/validate-query';
|
||||
import * as RolesService from '../services/roles';
|
||||
import useCollection from '../middleware/use-collection';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(useCollection('directus_roles'));
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
useCollection('directus_roles'),
|
||||
sanitizeQuery,
|
||||
validateQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
const primaryKey = await RolesService.createRole(req.body, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
user: req.user,
|
||||
});
|
||||
const item = await RolesService.readRole(primaryKey, req.sanitizedQuery);
|
||||
const item = await RolesService.readRole(primaryKey, req.sanitizedQuery, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
});
|
||||
return res.json({ data: item || null });
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
useCollection('directus_roles'),
|
||||
sanitizeQuery,
|
||||
validateQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
const records = await RolesService.readRoles(req.sanitizedQuery);
|
||||
const records = await RolesService.readRoles(req.sanitizedQuery, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
});
|
||||
return res.json({ data: records || null });
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:pk',
|
||||
useCollection('directus_roles'),
|
||||
sanitizeQuery,
|
||||
validateQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
const record = await RolesService.readRole(req.params.pk, req.sanitizedQuery);
|
||||
const record = await RolesService.readRole(req.params.pk, req.sanitizedQuery, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
});
|
||||
return res.json({ data: record || null });
|
||||
})
|
||||
);
|
||||
|
||||
router.patch(
|
||||
'/:pk',
|
||||
useCollection('directus_roles'),
|
||||
sanitizeQuery,
|
||||
validateQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
const primaryKey = await RolesService.updateRole(req.params.pk, req.body, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
user: req.user,
|
||||
});
|
||||
const item = await RolesService.readRole(primaryKey, req.sanitizedQuery);
|
||||
const item = await RolesService.readRole(primaryKey, req.sanitizedQuery, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
});
|
||||
return res.json({ data: item || null });
|
||||
})
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:pk',
|
||||
useCollection('directus_roles'),
|
||||
asyncHandler(async (req, res) => {
|
||||
await RolesService.deleteRole(req.params.pk, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
user: req.user,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import express from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import sanitizeQuery from '../middleware/sanitize-query';
|
||||
import validateQuery from '../middleware/validate-query';
|
||||
import * as SettingsService from '../services/settings';
|
||||
import useCollection from '../middleware/use-collection';
|
||||
|
||||
@@ -11,9 +10,11 @@ router.get(
|
||||
'/',
|
||||
useCollection('directus_settings'),
|
||||
sanitizeQuery,
|
||||
validateQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
const records = await SettingsService.readSettings(req.sanitizedQuery);
|
||||
const records = await SettingsService.readSettings(req.sanitizedQuery, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
});
|
||||
return res.json({ data: records || null });
|
||||
})
|
||||
);
|
||||
@@ -22,15 +23,19 @@ router.patch(
|
||||
'/',
|
||||
useCollection('directus_settings'),
|
||||
sanitizeQuery,
|
||||
validateQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
await SettingsService.updateSettings(req.body, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
user: req.user,
|
||||
});
|
||||
|
||||
const record = await SettingsService.readSettings(req.sanitizedQuery);
|
||||
const record = await SettingsService.readSettings(req.sanitizedQuery, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
});
|
||||
|
||||
return res.json({ data: record || null });
|
||||
})
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import express from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import sanitizeQuery from '../middleware/sanitize-query';
|
||||
import validateQuery from '../middleware/validate-query';
|
||||
import * as UsersService from '../services/users';
|
||||
import Joi from '@hapi/joi';
|
||||
import { InvalidPayloadException, InvalidCredentialsException } from '../exceptions';
|
||||
@@ -9,101 +8,116 @@ import useCollection from '../middleware/use-collection';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(useCollection('directus_users'));
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
useCollection('directus_users'),
|
||||
sanitizeQuery,
|
||||
validateQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
const primaryKey = await UsersService.createUser(req.body, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
user: req.user,
|
||||
});
|
||||
const item = await UsersService.readUser(primaryKey, req.sanitizedQuery);
|
||||
const item = await UsersService.readUser(primaryKey, req.sanitizedQuery, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
});
|
||||
return res.json({ data: item || null });
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
useCollection('directus_users'),
|
||||
sanitizeQuery,
|
||||
validateQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
const item = await UsersService.readUsers(req.sanitizedQuery);
|
||||
const item = await UsersService.readUsers(req.sanitizedQuery, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
});
|
||||
return res.json({ data: item || null });
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/me',
|
||||
useCollection('directus_users'),
|
||||
sanitizeQuery,
|
||||
validateQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
if (!req.user) {
|
||||
throw new InvalidCredentialsException();
|
||||
}
|
||||
|
||||
const item = await UsersService.readUser(req.user, req.sanitizedQuery);
|
||||
const item = await UsersService.readUser(req.user, req.sanitizedQuery, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
});
|
||||
return res.json({ data: item || null });
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:pk',
|
||||
useCollection('directus_users'),
|
||||
sanitizeQuery,
|
||||
validateQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
const items = await UsersService.readUser(req.params.pk, req.sanitizedQuery);
|
||||
const items = await UsersService.readUser(req.params.pk, req.sanitizedQuery, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
});
|
||||
return res.json({ data: items || null });
|
||||
})
|
||||
);
|
||||
|
||||
router.patch(
|
||||
'/me',
|
||||
useCollection('directus_users'),
|
||||
sanitizeQuery,
|
||||
validateQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
if (!req.user) {
|
||||
throw new InvalidCredentialsException();
|
||||
}
|
||||
|
||||
const primaryKey = await UsersService.updateUser(req.user, req.body, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
user: req.user,
|
||||
});
|
||||
|
||||
const item = await UsersService.readUser(primaryKey, req.sanitizedQuery);
|
||||
const item = await UsersService.readUser(primaryKey, req.sanitizedQuery, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
});
|
||||
return res.json({ data: item || null });
|
||||
})
|
||||
);
|
||||
|
||||
router.patch(
|
||||
'/:pk',
|
||||
useCollection('directus_users'),
|
||||
sanitizeQuery,
|
||||
validateQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
const primaryKey = await UsersService.updateUser(req.params.pk, req.body, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
user: req.user,
|
||||
});
|
||||
const item = await UsersService.readUser(primaryKey, req.sanitizedQuery);
|
||||
const item = await UsersService.readUser(primaryKey, req.sanitizedQuery, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
});
|
||||
return res.json({ data: item || null });
|
||||
})
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:pk',
|
||||
useCollection('directus_users'),
|
||||
asyncHandler(async (req, res) => {
|
||||
await UsersService.deleteUser(req.params.pk, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
user: req.user,
|
||||
@@ -120,11 +134,12 @@ const inviteSchema = Joi.object({
|
||||
|
||||
router.post(
|
||||
'/invite',
|
||||
useCollection('directus_users'),
|
||||
asyncHandler(async (req, res) => {
|
||||
const { error } = inviteSchema.validate(req.body);
|
||||
if (error) throw new InvalidPayloadException(error.message);
|
||||
await UsersService.inviteUser(req.body.email, req.body.role, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
user: req.user,
|
||||
@@ -140,7 +155,6 @@ const acceptInviteSchema = Joi.object({
|
||||
|
||||
router.post(
|
||||
'/invite/accept',
|
||||
useCollection('directus_users'),
|
||||
asyncHandler(async (req, res) => {
|
||||
const { error } = acceptInviteSchema.validate(req.body);
|
||||
if (error) throw new InvalidPayloadException(error.message);
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
import express from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import sanitizeQuery from '../middleware/sanitize-query';
|
||||
import validateQuery from '../middleware/validate-query';
|
||||
import * as WebhooksService from '../services/webhooks';
|
||||
import useCollection from '../middleware/use-collection';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(useCollection('directus_webhooks'));
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
useCollection('directus_webhooks'),
|
||||
asyncHandler(async (req, res) => {
|
||||
const primaryKey = await WebhooksService.createWebhook(req.body, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
user: req.user,
|
||||
});
|
||||
|
||||
const item = await WebhooksService.readWebhook(primaryKey, req.sanitizedQuery);
|
||||
const item = await WebhooksService.readWebhook(primaryKey, req.sanitizedQuery, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
});
|
||||
|
||||
return res.json({ data: item || null });
|
||||
})
|
||||
@@ -25,45 +30,52 @@ router.post(
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
useCollection('directus_webhooks'),
|
||||
sanitizeQuery,
|
||||
validateQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
const records = await WebhooksService.readWebhooks(req.sanitizedQuery);
|
||||
const records = await WebhooksService.readWebhooks(req.sanitizedQuery, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
});
|
||||
return res.json({ data: records || null });
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:pk',
|
||||
useCollection('directus_webhooks'),
|
||||
sanitizeQuery,
|
||||
validateQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
const record = await WebhooksService.readWebhook(req.params.pk, req.sanitizedQuery);
|
||||
const record = await WebhooksService.readWebhook(req.params.pk, req.sanitizedQuery, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
});
|
||||
return res.json({ data: record || null });
|
||||
})
|
||||
);
|
||||
|
||||
router.patch(
|
||||
'/:pk',
|
||||
useCollection('directus_webhooks'),
|
||||
asyncHandler(async (req, res) => {
|
||||
const primaryKey = await WebhooksService.updateWebhook(req.params.pk, req.body, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
user: req.user,
|
||||
});
|
||||
const item = await WebhooksService.readWebhook(primaryKey, req.sanitizedQuery);
|
||||
const item = await WebhooksService.readWebhook(primaryKey, req.sanitizedQuery, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
});
|
||||
return res.json({ data: item || null });
|
||||
})
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:pk',
|
||||
useCollection('directus_webhooks'),
|
||||
asyncHandler(async (req, res) => {
|
||||
await WebhooksService.deleteWebhook(req.params.pk, {
|
||||
role: req.role,
|
||||
admin: req.admin,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
user: req.user,
|
||||
|
||||
@@ -16,12 +16,16 @@ export const createActivity = async (data: Record<string, any>) => {
|
||||
return await ItemsService.createItem('directus_activity', data);
|
||||
};
|
||||
|
||||
export const readActivities = async (query?: Query) => {
|
||||
return await ItemsService.readItems('directus_activity', query);
|
||||
export const readActivities = async (query?: Query, accountability?: Accountability) => {
|
||||
return await ItemsService.readItems('directus_activity', query, accountability);
|
||||
};
|
||||
|
||||
export const readActivity = async (pk: string | number, query?: Query) => {
|
||||
return await ItemsService.readItem('directus_activity', pk, query);
|
||||
export const readActivity = async (
|
||||
pk: string | number,
|
||||
query?: Query,
|
||||
accountability?: Accountability
|
||||
) => {
|
||||
return await ItemsService.readItem('directus_activity', pk, query, accountability);
|
||||
};
|
||||
|
||||
export const updateActivity = async (
|
||||
|
||||
@@ -70,10 +70,10 @@ export const create = async (payload: any, accountability: Accountability) => {
|
||||
return await ItemsService.readItem('directus_collections', primaryKey);
|
||||
};
|
||||
|
||||
export const readAll = async (query?: Query) => {
|
||||
export const readAll = async (query?: Query, accountability?: Accountability) => {
|
||||
const [tables, collections] = await Promise.all([
|
||||
schemaInspector.tableInfo(),
|
||||
ItemsService.readItems<Collection>('directus_collections', query),
|
||||
ItemsService.readItems<Collection>('directus_collections', query, accountability),
|
||||
]);
|
||||
|
||||
const data = tables.map((table) => {
|
||||
@@ -94,10 +94,19 @@ export const readAll = async (query?: Query) => {
|
||||
return data;
|
||||
};
|
||||
|
||||
export const readOne = async (collection: string, query?: Query) => {
|
||||
export const readOne = async (
|
||||
collection: string,
|
||||
query?: Query,
|
||||
accountability?: Accountability
|
||||
) => {
|
||||
const [table, collectionInfo] = await Promise.all([
|
||||
schemaInspector.tableInfo(collection),
|
||||
ItemsService.readItem<Collection>('directus_collections', collection, query),
|
||||
ItemsService.readItem<Collection>(
|
||||
'directus_collections',
|
||||
collection,
|
||||
query,
|
||||
accountability
|
||||
),
|
||||
]);
|
||||
|
||||
return {
|
||||
|
||||
@@ -13,6 +13,11 @@ export const fieldsInCollection = async (collection: string) => {
|
||||
return uniq([...fields.map(({ field }) => field), ...columns.map(({ column }) => column)]);
|
||||
};
|
||||
|
||||
/**
|
||||
* @TODO
|
||||
* update read to use ItemsService instead of direct to db
|
||||
*/
|
||||
|
||||
export const readAll = async (collection?: string) => {
|
||||
const fieldsQuery = database.select('*').from('directus_fields');
|
||||
|
||||
|
||||
@@ -56,12 +56,16 @@ export const createFile = async (
|
||||
return await ItemsService.createItem('directus_files', payload, accountability);
|
||||
};
|
||||
|
||||
export const readFiles = async (query: Query) => {
|
||||
return await ItemsService.readItems('directus_files', query);
|
||||
export const readFiles = async (query: Query, accountability?: Accountability) => {
|
||||
return await ItemsService.readItems('directus_files', query, accountability);
|
||||
};
|
||||
|
||||
export const readFile = async (pk: string | number, query: Query) => {
|
||||
return await ItemsService.readItem('directus_files', pk, query);
|
||||
export const readFile = async (
|
||||
pk: string | number,
|
||||
query: Query,
|
||||
accountability?: Accountability
|
||||
) => {
|
||||
return await ItemsService.readItem('directus_files', pk, query, accountability);
|
||||
};
|
||||
|
||||
export const updateFile = async (
|
||||
|
||||
@@ -8,12 +8,12 @@ export const createFolder = async (
|
||||
return (await ItemsService.createItem('directus_folders', data, accountability)) as string;
|
||||
};
|
||||
|
||||
export const readFolders = async (query: Query) => {
|
||||
return await ItemsService.readItems('directus_folders', query);
|
||||
export const readFolders = async (query: Query, accountability?: Accountability) => {
|
||||
return await ItemsService.readItems('directus_folders', query, accountability);
|
||||
};
|
||||
|
||||
export const readFolder = async (pk: string, query: Query) => {
|
||||
return await ItemsService.readItem('directus_folders', pk, query);
|
||||
export const readFolder = async (pk: string, query: Query, accountability?: Accountability) => {
|
||||
return await ItemsService.readItem('directus_folders', pk, query, accountability);
|
||||
};
|
||||
|
||||
export const updateFolder = async (
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import database, { schemaInspector } from '../database';
|
||||
import { Query } from '../types/query';
|
||||
import runAST from '../database/run-ast';
|
||||
import getAST from '../utils/get-ast';
|
||||
import * as PayloadService from './payload';
|
||||
import { Accountability } from '../types/accountability';
|
||||
import getASTFromQuery from '../utils/get-ast-from-query';
|
||||
import { Accountability, Operation } from '../types';
|
||||
|
||||
import * as PayloadService from './payload';
|
||||
import * as PermissionsService from './permissions';
|
||||
import * as ActivityService from './activity';
|
||||
import * as RevisionsService from './revisions';
|
||||
|
||||
@@ -27,15 +28,17 @@ async function saveActivityAndRevision(
|
||||
action_by: accountability.user,
|
||||
});
|
||||
|
||||
await RevisionsService.createRevision({
|
||||
activity: activityID,
|
||||
collection,
|
||||
item,
|
||||
delta: payload,
|
||||
/** @todo make this configurable */
|
||||
data: await readItem(collection, item, { fields: ['*'] }),
|
||||
parent: accountability.parent,
|
||||
});
|
||||
if (action !== ActivityService.Action.DELETE) {
|
||||
await RevisionsService.createRevision({
|
||||
activity: activityID,
|
||||
collection,
|
||||
item,
|
||||
delta: payload,
|
||||
/** @todo make this configurable */
|
||||
data: await readItem(collection, item, { fields: ['*'] }),
|
||||
parent: accountability.parent,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const createItem = async (
|
||||
@@ -43,15 +46,24 @@ export const createItem = async (
|
||||
data: Record<string, any>,
|
||||
accountability?: Accountability
|
||||
): Promise<string | number> => {
|
||||
let payload = await PayloadService.processValues('create', collection, data);
|
||||
let payload = data;
|
||||
|
||||
if (accountability && accountability.admin === false) {
|
||||
payload = await PermissionsService.processValues(
|
||||
'create',
|
||||
collection,
|
||||
accountability?.role,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
payload = await PayloadService.processValues('create', collection, payload);
|
||||
|
||||
payload = await PayloadService.processM2O(collection, payload);
|
||||
|
||||
const primaryKeyField = await schemaInspector.primary(collection);
|
||||
|
||||
// Only insert the values that actually save to an existing column. This ensures we ignore aliases etc
|
||||
const columns = await schemaInspector.columns(collection);
|
||||
|
||||
const payloadWithoutAlias = pick(
|
||||
payload,
|
||||
columns.map(({ column }) => column)
|
||||
@@ -82,9 +94,15 @@ export const createItem = async (
|
||||
|
||||
export const readItems = async <T = Record<string, any>>(
|
||||
collection: string,
|
||||
query: Query = {}
|
||||
query: Query,
|
||||
accountability?: Accountability
|
||||
): Promise<T[]> => {
|
||||
const ast = await getAST(collection, query);
|
||||
let ast = await getASTFromQuery(collection, query, accountability);
|
||||
|
||||
if (accountability && accountability.admin === false) {
|
||||
ast = await PermissionsService.processAST(ast, accountability.role);
|
||||
}
|
||||
|
||||
const records = await runAST(ast);
|
||||
return await PayloadService.processValues('read', collection, records);
|
||||
};
|
||||
@@ -92,23 +110,32 @@ export const readItems = async <T = Record<string, any>>(
|
||||
export const readItem = async <T = any>(
|
||||
collection: string,
|
||||
pk: number | string,
|
||||
query: Query = {}
|
||||
query: Query = {},
|
||||
accountability?: Accountability,
|
||||
operation?: Operation
|
||||
): Promise<T> => {
|
||||
// We allow overriding the operation, so we can use the item read logic to validate permissions
|
||||
// for update and delete as well
|
||||
operation = operation || 'read';
|
||||
|
||||
const primaryKeyField = await schemaInspector.primary(collection);
|
||||
|
||||
query = {
|
||||
...query,
|
||||
filter: [
|
||||
...(query.filter || []),
|
||||
{
|
||||
column: primaryKeyField,
|
||||
operator: 'eq',
|
||||
value: pk,
|
||||
filter: {
|
||||
...(query.filter || {}),
|
||||
[primaryKeyField]: {
|
||||
_eq: pk,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const ast = await getAST(collection, query);
|
||||
let ast = await getASTFromQuery(collection, query, accountability, operation);
|
||||
|
||||
if (accountability && accountability.admin === false) {
|
||||
ast = await PermissionsService.processAST(ast, accountability.role, operation);
|
||||
}
|
||||
|
||||
const records = await runAST(ast);
|
||||
return await PayloadService.processValues('read', collection, records[0]);
|
||||
};
|
||||
@@ -119,8 +146,20 @@ export const updateItem = async (
|
||||
data: Record<string, any>,
|
||||
accountability?: Accountability
|
||||
): Promise<string | number> => {
|
||||
let payload = await PayloadService.processValues('update', collection, data);
|
||||
let payload = data;
|
||||
|
||||
if (accountability && accountability.admin === false) {
|
||||
await PermissionsService.checkAccess('update', collection, pk, accountability.role);
|
||||
|
||||
payload = await PermissionsService.processValues(
|
||||
'validate',
|
||||
collection,
|
||||
accountability.role,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
payload = await PayloadService.processValues('update', collection, payload);
|
||||
payload = await PayloadService.processM2O(collection, payload);
|
||||
|
||||
const primaryKeyField = await schemaInspector.primary(collection);
|
||||
@@ -158,7 +197,9 @@ export const deleteItem = async (
|
||||
) => {
|
||||
const primaryKeyField = await schemaInspector.primary(collection);
|
||||
|
||||
if (accountability) {
|
||||
if (accountability && accountability.admin === false) {
|
||||
await PermissionsService.checkAccess('delete', collection, pk, accountability.role);
|
||||
|
||||
// Don't await this. It can run async in the background
|
||||
saveActivityAndRevision(
|
||||
ActivityService.Action.DELETE,
|
||||
@@ -174,8 +215,14 @@ export const deleteItem = async (
|
||||
.where({ [primaryKeyField]: pk });
|
||||
};
|
||||
|
||||
export const readSingleton = async (collection: string, query: Query = {}) => {
|
||||
const records = await readItems(collection, { ...query, limit: 1 });
|
||||
export const readSingleton = async (
|
||||
collection: string,
|
||||
query: Query,
|
||||
accountability?: Accountability
|
||||
) => {
|
||||
query.limit = 1;
|
||||
|
||||
const records = await readItems(collection, query, accountability);
|
||||
const record = records[0];
|
||||
|
||||
if (!record) {
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
import { Accountability, Query } from '../types';
|
||||
import {
|
||||
Accountability,
|
||||
AST,
|
||||
NestedCollectionAST,
|
||||
FieldAST,
|
||||
Query,
|
||||
Permission,
|
||||
Operation,
|
||||
} from '../types';
|
||||
import * as ItemsService from './items';
|
||||
import database from '../database';
|
||||
import { ForbiddenException, InvalidPayloadException } from '../exceptions';
|
||||
import { uniq } from 'lodash';
|
||||
import generateJoi from '../utils/generate-joi';
|
||||
|
||||
export const createPermission = async (
|
||||
data: Record<string, any>,
|
||||
@@ -8,12 +20,12 @@ export const createPermission = async (
|
||||
return (await ItemsService.createItem('directus_permissions', data, accountability)) as number;
|
||||
};
|
||||
|
||||
export const readPermissions = async (query: Query) => {
|
||||
return await ItemsService.readItems('directus_permissions', query);
|
||||
export const readPermissions = async (query: Query, accountability?: Accountability) => {
|
||||
return await ItemsService.readItems('directus_permissions', query, accountability);
|
||||
};
|
||||
|
||||
export const readPermission = async (pk: number, query: Query) => {
|
||||
return await ItemsService.readItem('directus_permissions', pk, query);
|
||||
export const readPermission = async (pk: number, query: Query, accountability?: Accountability) => {
|
||||
return await ItemsService.readItem('directus_permissions', pk, query, accountability);
|
||||
};
|
||||
|
||||
export const updatePermission = async (
|
||||
@@ -32,3 +44,201 @@ export const updatePermission = async (
|
||||
export const deletePermission = async (pk: number, accountability: Accountability) => {
|
||||
await ItemsService.deleteItem('directus_permissions', pk, accountability);
|
||||
};
|
||||
|
||||
export const processAST = async (
|
||||
ast: AST,
|
||||
role: string | null,
|
||||
operation: Operation = 'read'
|
||||
): Promise<AST> => {
|
||||
const collectionsRequested = getCollectionsFromAST(ast);
|
||||
|
||||
const permissionsForCollections = await database
|
||||
.select<Permission[]>('*')
|
||||
.from('directus_permissions')
|
||||
.where({ operation, role })
|
||||
.whereIn(
|
||||
'collection',
|
||||
collectionsRequested.map(({ collection }) => collection)
|
||||
);
|
||||
|
||||
// If the permissions don't match the collections, you don't have permission to read all of them
|
||||
const uniqueCollectionsRequestedCount = uniq(
|
||||
collectionsRequested.map(({ collection }) => collection)
|
||||
).length;
|
||||
|
||||
if (uniqueCollectionsRequestedCount !== permissionsForCollections.length) {
|
||||
// Find the first collection that doesn't have permissions configured
|
||||
const { collection, field } = collectionsRequested.find(
|
||||
({ collection }) =>
|
||||
permissionsForCollections.find(
|
||||
(permission) => permission.collection === collection
|
||||
) === undefined
|
||||
);
|
||||
|
||||
if (field) {
|
||||
throw new ForbiddenException(
|
||||
`You don't have permission to access the "${field}" field.`
|
||||
);
|
||||
} else {
|
||||
throw new ForbiddenException(
|
||||
`You don't have permission to access the "${collection}" collection.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
validateFields(ast);
|
||||
|
||||
applyFilters(ast);
|
||||
|
||||
return ast;
|
||||
|
||||
/**
|
||||
* Traverses the AST and returns an array of all collections that are being fetched
|
||||
*/
|
||||
function getCollectionsFromAST(ast: AST | NestedCollectionAST) {
|
||||
const collections = [];
|
||||
|
||||
if (ast.type === 'collection') {
|
||||
collections.push({
|
||||
collection: ast.name,
|
||||
field: (ast as NestedCollectionAST).fieldKey
|
||||
? (ast as NestedCollectionAST).fieldKey
|
||||
: null,
|
||||
});
|
||||
}
|
||||
|
||||
for (const subAST of ast.children) {
|
||||
if (subAST.type === 'collection') {
|
||||
collections.push(...getCollectionsFromAST(subAST));
|
||||
}
|
||||
}
|
||||
|
||||
return collections;
|
||||
}
|
||||
|
||||
function validateFields(ast: AST | NestedCollectionAST) {
|
||||
if (ast.type === 'collection') {
|
||||
const collection = ast.name;
|
||||
const permissions = permissionsForCollections.find(
|
||||
(permission) => permission.collection === collection
|
||||
);
|
||||
const allowedFields = permissions.fields.split(',');
|
||||
|
||||
for (const childAST of ast.children) {
|
||||
if (childAST.type === 'collection') {
|
||||
validateFields(childAST);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (allowedFields.includes('*')) continue;
|
||||
|
||||
const fieldKey = childAST.name;
|
||||
|
||||
if (allowedFields.includes(fieldKey) === false) {
|
||||
throw new ForbiddenException(
|
||||
`You don't have permission to access the "${fieldKey}" field.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function applyFilters(
|
||||
ast: AST | NestedCollectionAST | FieldAST
|
||||
): AST | NestedCollectionAST | FieldAST {
|
||||
if (ast.type === 'collection') {
|
||||
const collection = ast.name;
|
||||
|
||||
const permissions = permissionsForCollections.find(
|
||||
(permission) => permission.collection === collection
|
||||
);
|
||||
|
||||
ast.query = {
|
||||
...ast.query,
|
||||
filter: {
|
||||
...(ast.query.filter || {}),
|
||||
...permissions.permissions,
|
||||
},
|
||||
};
|
||||
|
||||
if (permissions.limit && ast.query.limit > permissions.limit) {
|
||||
throw new ForbiddenException(
|
||||
`You can't read more than ${permissions.limit} items at a time.`
|
||||
);
|
||||
}
|
||||
|
||||
// Default to the permissions limit if limit hasn't been set
|
||||
if (permissions.limit && !ast.query.limit) {
|
||||
ast.query.limit = permissions.limit;
|
||||
}
|
||||
|
||||
ast.children = ast.children.map(applyFilters) as (NestedCollectionAST | FieldAST)[];
|
||||
}
|
||||
|
||||
return ast;
|
||||
}
|
||||
};
|
||||
|
||||
export const processValues = async (
|
||||
operation: Operation,
|
||||
collection: string,
|
||||
role: string | null,
|
||||
data: Record<string, any>
|
||||
) => {
|
||||
const permission = await database
|
||||
.select<Permission>('*')
|
||||
.from('directus_permissions')
|
||||
.where({ operation, collection, role })
|
||||
.first();
|
||||
|
||||
if (!permission) throw new ForbiddenException();
|
||||
|
||||
const allowedFields = permission.fields.split(',');
|
||||
|
||||
if (allowedFields.includes('*') === false) {
|
||||
const keysInData = Object.keys(data);
|
||||
const invalidKeys = keysInData.filter(
|
||||
(fieldKey) => allowedFields.includes(fieldKey) === false
|
||||
);
|
||||
|
||||
if (invalidKeys.length > 0) {
|
||||
throw new InvalidPayloadException(`Field "${invalidKeys[0]}" doesn't exist.`);
|
||||
}
|
||||
}
|
||||
|
||||
const preset = permission.presets || {};
|
||||
|
||||
const payload = {
|
||||
...preset,
|
||||
...data,
|
||||
};
|
||||
|
||||
const schema = generateJoi(permission.permissions);
|
||||
|
||||
const { error } = schema.validate(payload);
|
||||
|
||||
if (error) {
|
||||
throw new InvalidPayloadException(error.message);
|
||||
}
|
||||
|
||||
return payload;
|
||||
};
|
||||
|
||||
export const checkAccess = async (
|
||||
operation: Operation,
|
||||
collection: string,
|
||||
pk: string | number,
|
||||
role: string
|
||||
) => {
|
||||
try {
|
||||
const query: Query = {
|
||||
fields: ['*'],
|
||||
};
|
||||
|
||||
const result = await ItemsService.readItem(collection, pk, query, { role }, operation);
|
||||
|
||||
if (!result) throw '';
|
||||
} catch {
|
||||
throw new ForbiddenException(`You're not allowed to ${operation} this item.`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -10,12 +10,16 @@ export const createCollectionPreset = async (
|
||||
return await ItemsService.createItem('directus_presets', data, accountability);
|
||||
};
|
||||
|
||||
export const readCollectionPresets = async (query: Query) => {
|
||||
return await ItemsService.readItems('directus_presets', query);
|
||||
export const readCollectionPresets = async (query: Query, accountability?: Accountability) => {
|
||||
return await ItemsService.readItems('directus_presets', query, accountability);
|
||||
};
|
||||
|
||||
export const readCollectionPreset = async (pk: string | number, query: Query) => {
|
||||
return await ItemsService.readItem('directus_presets', pk, query);
|
||||
export const readCollectionPreset = async (
|
||||
pk: string | number,
|
||||
query: Query,
|
||||
accountability?: Accountability
|
||||
) => {
|
||||
return await ItemsService.readItem('directus_presets', pk, query, accountability);
|
||||
};
|
||||
|
||||
export const updateCollectionPreset = async (
|
||||
|
||||
@@ -5,12 +5,16 @@ export const createRelation = async (data: Record<string, any>, accountability:
|
||||
return await ItemsService.createItem('directus_relations', data, accountability);
|
||||
};
|
||||
|
||||
export const readRelations = async (query: Query) => {
|
||||
return await ItemsService.readItems('directus_relations', query);
|
||||
export const readRelations = async (query: Query, accountability?: Accountability) => {
|
||||
return await ItemsService.readItems('directus_relations', query, accountability);
|
||||
};
|
||||
|
||||
export const readRelation = async (pk: string | number, query: Query) => {
|
||||
return await ItemsService.readItem('directus_relations', pk, query);
|
||||
export const readRelation = async (
|
||||
pk: string | number,
|
||||
query: Query,
|
||||
accountability?: Accountability
|
||||
) => {
|
||||
return await ItemsService.readItem('directus_relations', pk, query, accountability);
|
||||
};
|
||||
|
||||
export const updateRelation = async (
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import { Query } from '../types/query';
|
||||
import * as ItemsService from './items';
|
||||
import { Accountability, Query } from '../types';
|
||||
|
||||
export const createRevision = async (data: Record<string, any>) => {
|
||||
return await ItemsService.createItem('directus_revisions', data);
|
||||
};
|
||||
|
||||
export const readRevisions = async (query: Query) => {
|
||||
return await ItemsService.readItems('directus_revisions', query);
|
||||
export const readRevisions = async (query: Query, accountability?: Accountability) => {
|
||||
return await ItemsService.readItems('directus_revisions', query, accountability);
|
||||
};
|
||||
|
||||
export const readRevision = async (pk: string | number, query: Query) => {
|
||||
return await ItemsService.readItem('directus_revisions', pk, query);
|
||||
export const readRevision = async (
|
||||
pk: string | number,
|
||||
query: Query,
|
||||
accountability?: Accountability
|
||||
) => {
|
||||
return await ItemsService.readItem('directus_revisions', pk, query, accountability);
|
||||
};
|
||||
|
||||
@@ -5,12 +5,16 @@ export const createRole = async (data: Record<string, any>, accountability: Acco
|
||||
return await ItemsService.createItem('directus_roles', data, accountability);
|
||||
};
|
||||
|
||||
export const readRoles = async (query: Query) => {
|
||||
return await ItemsService.readItems('directus_roles', query);
|
||||
export const readRoles = async (query: Query, accountability?: Accountability) => {
|
||||
return await ItemsService.readItems('directus_roles', query, accountability);
|
||||
};
|
||||
|
||||
export const readRole = async (pk: string | number, query: Query) => {
|
||||
return await ItemsService.readItem('directus_roles', pk, query);
|
||||
export const readRole = async (
|
||||
pk: string | number,
|
||||
query: Query,
|
||||
accountability?: Accountability
|
||||
) => {
|
||||
return await ItemsService.readItem('directus_roles', pk, query, accountability);
|
||||
};
|
||||
|
||||
export const updateRole = async (
|
||||
|
||||
@@ -2,8 +2,8 @@ import { Query } from '../types/query';
|
||||
import * as ItemsService from './items';
|
||||
import { Accountability } from '../types';
|
||||
|
||||
export const readSettings = async (query: Query) => {
|
||||
return await ItemsService.readSingleton('directus_settings', query);
|
||||
export const readSettings = async (query: Query, accountability?: Accountability) => {
|
||||
return await ItemsService.readSingleton('directus_settings', query, accountability);
|
||||
};
|
||||
|
||||
export const updateSettings = async (data: Record<string, any>, accountability: Accountability) => {
|
||||
|
||||
@@ -10,12 +10,16 @@ export const createUser = async (data: Record<string, any>, accountability: Acco
|
||||
return await ItemsService.createItem('directus_users', data, accountability);
|
||||
};
|
||||
|
||||
export const readUsers = async (query?: Query) => {
|
||||
return await ItemsService.readItems('directus_users', query);
|
||||
export const readUsers = async (query?: Query, accountability?: Accountability) => {
|
||||
return await ItemsService.readItems('directus_users', query, accountability);
|
||||
};
|
||||
|
||||
export const readUser = async (pk: string | number, query?: Query) => {
|
||||
return await ItemsService.readItem('directus_users', pk, query);
|
||||
export const readUser = async (
|
||||
pk: string | number,
|
||||
query?: Query,
|
||||
accountability?: Accountability
|
||||
) => {
|
||||
return await ItemsService.readItem('directus_users', pk, query, accountability);
|
||||
};
|
||||
|
||||
export const updateUser = async (
|
||||
|
||||
@@ -5,12 +5,16 @@ export const createWebhook = async (data: Record<string, any>, accountability: A
|
||||
return await ItemsService.createItem('directus_webhooks', data, accountability);
|
||||
};
|
||||
|
||||
export const readWebhooks = async (query: Query) => {
|
||||
return await ItemsService.readItems('directus_webhooks', query);
|
||||
export const readWebhooks = async (query: Query, accountability?: Accountability) => {
|
||||
return await ItemsService.readItems('directus_webhooks', query, accountability);
|
||||
};
|
||||
|
||||
export const readWebhook = async (pk: string | number, query: Query) => {
|
||||
return await ItemsService.readItem('directus_webhooks', pk, query);
|
||||
export const readWebhook = async (
|
||||
pk: string | number,
|
||||
query: Query,
|
||||
accountability?: Accountability
|
||||
) => {
|
||||
return await ItemsService.readItem('directus_webhooks', pk, query, accountability);
|
||||
};
|
||||
|
||||
export const updateWebhook = async (
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
export type Accountability = {
|
||||
role: string;
|
||||
user?: string;
|
||||
admin?: boolean;
|
||||
|
||||
ip?: string;
|
||||
userAgent?: string;
|
||||
user?: string;
|
||||
|
||||
parent?: number;
|
||||
};
|
||||
|
||||
12
src/types/express.d.ts
vendored
12
src/types/express.d.ts
vendored
@@ -2,17 +2,21 @@
|
||||
* Custom properties on the req object in express
|
||||
*/
|
||||
|
||||
import { Permission } from './permissions';
|
||||
|
||||
export {};
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
export interface Request {
|
||||
token?: string;
|
||||
user?: string;
|
||||
role?: string;
|
||||
token: string;
|
||||
user: string;
|
||||
role: string | null;
|
||||
admin: boolean;
|
||||
collection?: string;
|
||||
sanitizedQuery?: Record<string, any>;
|
||||
single: boolean;
|
||||
single?: boolean;
|
||||
permissions?: Permission;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ export * from './collection';
|
||||
export * from './field';
|
||||
export * from './files';
|
||||
export * from './meta';
|
||||
export * from './permissions';
|
||||
export * from './query';
|
||||
export * from './relation';
|
||||
export * from './sessions';
|
||||
|
||||
19
src/types/permissions.ts
Normal file
19
src/types/permissions.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export type Operation =
|
||||
| 'create'
|
||||
| 'read'
|
||||
| 'update'
|
||||
| 'validate'
|
||||
| 'delete'
|
||||
| 'comment'
|
||||
| 'explain';
|
||||
|
||||
export type Permission = {
|
||||
id: number;
|
||||
role: string | null;
|
||||
collection: string;
|
||||
operation: Operation;
|
||||
permissions: Record<string, any>;
|
||||
limit: number | null;
|
||||
presets: Record<string, any> | null;
|
||||
fields: string | null;
|
||||
};
|
||||
@@ -3,7 +3,7 @@ import { Meta } from './meta';
|
||||
export type Query = {
|
||||
fields?: string[];
|
||||
sort?: Sort[];
|
||||
filter?: Filter[];
|
||||
filter?: Filter;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
page?: number;
|
||||
@@ -18,9 +18,7 @@ export type Sort = {
|
||||
};
|
||||
|
||||
export type Filter = {
|
||||
column: string;
|
||||
operator: FilterOperator;
|
||||
value: null | string | number | (string | number)[];
|
||||
[keyOrOperator: string]: Filter | any;
|
||||
};
|
||||
|
||||
export type FilterOperator = 'eq' | 'neq' | 'in' | 'nin' | 'null' | 'nnull';
|
||||
|
||||
44
src/utils/generate-joi.ts
Normal file
44
src/utils/generate-joi.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Filter } from '../types';
|
||||
import Joi, { AnySchema } from '@hapi/joi';
|
||||
|
||||
export default function generateJoi(filter: Filter) {
|
||||
const schema: Record<string, AnySchema> = {};
|
||||
|
||||
for (const [key, value] of Object.entries(filter)) {
|
||||
const isField = key.startsWith('_') === false;
|
||||
|
||||
if (isField) {
|
||||
const operator = Object.keys(value)[0];
|
||||
|
||||
/** @TODO
|
||||
* - Extend with all operators
|
||||
*/
|
||||
|
||||
if (operator === '_eq') {
|
||||
schema[key] = Joi.any().equal(Object.values(value)[0]);
|
||||
}
|
||||
|
||||
if (operator === '_neq') {
|
||||
schema[key] = Joi.any().not(Object.values(value)[0]);
|
||||
}
|
||||
|
||||
if (operator === '_in') {
|
||||
schema[key] = Joi.any().equal(...(Object.values(value)[0] as (string | number)[]));
|
||||
}
|
||||
|
||||
if (operator === '_nin') {
|
||||
schema[key] = Joi.any().not(...(Object.values(value)[0] as (string | number)[]));
|
||||
}
|
||||
|
||||
if (operator === '_gt') {
|
||||
schema[key] = Joi.number().greater(Number(Object.values(value)[0]));
|
||||
}
|
||||
|
||||
if (operator === '_lt') {
|
||||
schema[key] = Joi.number().less(Number(Object.values(value)[0]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Joi.object(schema).unknown();
|
||||
}
|
||||
182
src/utils/get-ast-from-query.ts
Normal file
182
src/utils/get-ast-from-query.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* Generate an AST based on a given collection and query
|
||||
*/
|
||||
|
||||
import {
|
||||
AST,
|
||||
NestedCollectionAST,
|
||||
FieldAST,
|
||||
Query,
|
||||
Relation,
|
||||
Operation,
|
||||
Accountability,
|
||||
} from '../types';
|
||||
import database from '../database';
|
||||
|
||||
export default async function getASTFromQuery(
|
||||
collection: string,
|
||||
query: Query,
|
||||
accountability: Accountability | null,
|
||||
operation?: Operation
|
||||
): Promise<AST> {
|
||||
/**
|
||||
* we might not need al this info at all times, but it's easier to fetch it all once, than trying to fetch it for every
|
||||
* requested field. @todo look into utilizing graphql/dataloader for this purpose
|
||||
*/
|
||||
const relations = await database.select<Relation[]>('*').from('directus_relations');
|
||||
|
||||
const permissions =
|
||||
accountability && accountability.admin !== true
|
||||
? await database
|
||||
.select<{ collection: string; fields: string }[]>('collection', 'fields')
|
||||
.from('directus_permissions')
|
||||
.where({ role: accountability.role, operation: operation || 'read' })
|
||||
: null;
|
||||
|
||||
const ast: AST = {
|
||||
type: 'collection',
|
||||
name: collection,
|
||||
query: query,
|
||||
children: [],
|
||||
};
|
||||
|
||||
const fields = query.fields || ['*'];
|
||||
|
||||
// Prevent fields from showing up in the query object
|
||||
delete query.fields;
|
||||
|
||||
ast.children = parseFields(collection, fields).filter(filterEmptyChildCollections);
|
||||
|
||||
return ast;
|
||||
|
||||
function convertWildcards(parentCollection: string, fields: string[]) {
|
||||
const allowedFields = permissions
|
||||
? permissions
|
||||
.find((permission) => parentCollection === permission.collection)
|
||||
?.fields?.split(',')
|
||||
: ['*'];
|
||||
|
||||
if (!allowedFields || allowedFields.length === 0) return [];
|
||||
|
||||
for (let index = 0; index < fields.length; index++) {
|
||||
const fieldKey = fields[index];
|
||||
|
||||
if (fieldKey.includes('*') === false) continue;
|
||||
|
||||
if (fieldKey === '*') {
|
||||
if (allowedFields.includes('*')) continue;
|
||||
fields.splice(index, 1, ...allowedFields);
|
||||
}
|
||||
|
||||
// Swap *.* case for *,<relational-field>.*,<another-relational>.*
|
||||
if (fieldKey.includes('.') && fieldKey.split('.')[0] === '*') {
|
||||
const parts = fieldKey.split('.');
|
||||
|
||||
const relationalFields = allowedFields.includes('*')
|
||||
? relations
|
||||
.filter(
|
||||
(relation) =>
|
||||
relation.collection_many === parentCollection ||
|
||||
relation.collection_one === parentCollection
|
||||
)
|
||||
.map((relation) => {
|
||||
const isM2O = relation.collection_many === parentCollection;
|
||||
return isM2O ? relation.field_many : relation.field_one;
|
||||
})
|
||||
: allowedFields.filter((fieldKey) => !!getRelation(parentCollection, fieldKey));
|
||||
|
||||
const nonRelationalFields = allowedFields.filter(
|
||||
(fieldKey) => relationalFields.includes(fieldKey) === false
|
||||
);
|
||||
|
||||
fields.splice(
|
||||
index,
|
||||
1,
|
||||
...[
|
||||
...relationalFields.map((relationalField) => {
|
||||
return `${relationalField}.${parts.slice(1).join('.')}`;
|
||||
}),
|
||||
...nonRelationalFields,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
function parseFields(parentCollection: string, fields: string[]) {
|
||||
fields = convertWildcards(parentCollection, fields);
|
||||
|
||||
if (!fields) return null;
|
||||
|
||||
const children: (NestedCollectionAST | FieldAST)[] = [];
|
||||
|
||||
const relationalStructure: Record<string, string[]> = {};
|
||||
|
||||
for (const field of fields) {
|
||||
if (field.includes('.') === false) {
|
||||
children.push({ type: 'field', name: field });
|
||||
} else {
|
||||
// field is relational
|
||||
const parts = field.split('.');
|
||||
|
||||
if (relationalStructure.hasOwnProperty(parts[0]) === false) {
|
||||
relationalStructure[parts[0]] = [];
|
||||
}
|
||||
|
||||
relationalStructure[parts[0]].push(parts.slice(1).join('.'));
|
||||
}
|
||||
}
|
||||
|
||||
for (const [relationalField, nestedFields] of Object.entries(relationalStructure)) {
|
||||
const relatedCollection = getRelatedCollection(parentCollection, relationalField);
|
||||
|
||||
const child: NestedCollectionAST = {
|
||||
type: 'collection',
|
||||
name: relatedCollection,
|
||||
fieldKey: relationalField,
|
||||
parentKey: 'id' /** @todo this needs to come from somewhere real */,
|
||||
relation: getRelation(parentCollection, relationalField),
|
||||
query: {} /** @todo inject nested query here */,
|
||||
children: parseFields(relatedCollection, nestedFields).filter(
|
||||
filterEmptyChildCollections
|
||||
),
|
||||
};
|
||||
|
||||
children.push(child);
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
function getRelation(collection: string, field: string) {
|
||||
const relation = relations.find((relation) => {
|
||||
return (
|
||||
(relation.collection_many === collection && relation.field_many === field) ||
|
||||
(relation.collection_one === collection && relation.field_one === field)
|
||||
);
|
||||
});
|
||||
|
||||
return relation;
|
||||
}
|
||||
|
||||
function getRelatedCollection(collection: string, field: string) {
|
||||
const relation = getRelation(collection, field);
|
||||
|
||||
if (!relation) return null;
|
||||
|
||||
if (relation.collection_many === collection && relation.field_many === field) {
|
||||
return relation.collection_one;
|
||||
}
|
||||
|
||||
if (relation.collection_one === collection && relation.field_one === field) {
|
||||
return relation.collection_many;
|
||||
}
|
||||
}
|
||||
|
||||
function filterEmptyChildCollections(childAST: NestedCollectionAST) {
|
||||
if (childAST.type === 'collection' && childAST.children.length === 0) return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
/**
|
||||
* Generate an AST based on a given collection and query
|
||||
*/
|
||||
|
||||
import { Query } from '../types/query';
|
||||
import { Relation } from '../types/relation';
|
||||
import { AST, NestedCollectionAST, FieldAST } from '../types/ast';
|
||||
import database from '../database';
|
||||
import * as FieldsService from '../services/fields';
|
||||
|
||||
export default async function getAST(collection: string, query: Query): Promise<AST> {
|
||||
const ast: AST = {
|
||||
type: 'collection',
|
||||
name: collection,
|
||||
query: query,
|
||||
children: [],
|
||||
};
|
||||
|
||||
if (!query.fields) query.fields = ['*'];
|
||||
|
||||
/** @todo support wildcard */
|
||||
const fields = query.fields;
|
||||
|
||||
// If no relational fields are requested, we can stop early
|
||||
const hasRelations = query.fields.some((field) => field.includes('.'));
|
||||
if (hasRelations === false) {
|
||||
fields.forEach((field) => {
|
||||
ast.children.push({
|
||||
type: 'field',
|
||||
name: field,
|
||||
});
|
||||
});
|
||||
|
||||
return ast;
|
||||
}
|
||||
|
||||
// Even though we might not need all records from relations, it'll be faster to load all records
|
||||
// into memory once and search through it in JS than it would be to do individual queries to fetch
|
||||
// this data field by field
|
||||
const relations = await database.select<Relation[]>('*').from('directus_relations');
|
||||
|
||||
ast.children = await parseFields(collection, query.fields);
|
||||
|
||||
return ast;
|
||||
|
||||
async function parseFields(parentCollection: string, fields: string[]) {
|
||||
const children: (NestedCollectionAST | FieldAST)[] = [];
|
||||
|
||||
const relationalStructure: Record<string, string[]> = {};
|
||||
|
||||
// Swap *.* case for *,<relational-field>.*
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
const fieldKey = fields[i];
|
||||
|
||||
if (fieldKey.includes('.') === false) continue;
|
||||
|
||||
const parts = fieldKey.split('.');
|
||||
|
||||
if (parts[0] === '*') {
|
||||
const availableFields = await FieldsService.fieldsInCollection(parentCollection);
|
||||
fields.splice(
|
||||
i,
|
||||
1,
|
||||
...availableFields
|
||||
.filter((field) => !!getRelation(parentCollection, field))
|
||||
.map((field) => `${field}.${parts.slice(1).join('.')}`)
|
||||
);
|
||||
fields.push('*');
|
||||
}
|
||||
}
|
||||
|
||||
for (const field of fields) {
|
||||
if (field.includes('.') === false) {
|
||||
children.push({ type: 'field', name: field });
|
||||
} else {
|
||||
// field is relational
|
||||
const parts = field.split('.');
|
||||
|
||||
if (relationalStructure.hasOwnProperty(parts[0]) === false) {
|
||||
relationalStructure[parts[0]] = [];
|
||||
}
|
||||
|
||||
relationalStructure[parts[0]].push(parts.slice(1).join('.'));
|
||||
}
|
||||
}
|
||||
|
||||
for (const [relationalField, nestedFields] of Object.entries(relationalStructure)) {
|
||||
const relatedCollection = getRelatedCollection(parentCollection, relationalField);
|
||||
|
||||
const child: NestedCollectionAST = {
|
||||
type: 'collection',
|
||||
name: relatedCollection,
|
||||
fieldKey: relationalField,
|
||||
parentKey: 'id' /** @todo this needs to come from somewhere real */,
|
||||
relation: getRelation(parentCollection, relationalField),
|
||||
query: {} /** @todo inject nested query here */,
|
||||
children: await parseFields(relatedCollection, nestedFields),
|
||||
};
|
||||
|
||||
children.push(child);
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
function getRelation(collection: string, field: string) {
|
||||
const relation = relations.find((relation) => {
|
||||
return (
|
||||
(relation.collection_many === collection && relation.field_many === field) ||
|
||||
(relation.collection_one === collection && relation.field_one === field)
|
||||
);
|
||||
});
|
||||
|
||||
return relation;
|
||||
}
|
||||
|
||||
function getRelatedCollection(collection: string, field: string) {
|
||||
const relation = getRelation(collection, field);
|
||||
|
||||
if (!relation) return null;
|
||||
|
||||
if (relation.collection_many === collection && relation.field_many === field) {
|
||||
return relation.collection_one;
|
||||
}
|
||||
|
||||
if (relation.collection_one === collection && relation.field_one === field) {
|
||||
return relation.collection_many;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import database, { schemaInspector } from '../database';
|
||||
import { FIELD_SPECIAL_ALIAS_TYPES } from '../constants';
|
||||
import { uniq } from 'lodash';
|
||||
|
||||
export default async function hasFields(fields: { collection: string; field: string }[]) {
|
||||
@@ -28,8 +27,7 @@ export async function collectionHasFields(collection: string, fieldKeys: string[
|
||||
.select('field')
|
||||
.from('directus_fields')
|
||||
.where({ collection })
|
||||
.whereIn('field', fieldKeys)
|
||||
.whereIn('special', FIELD_SPECIAL_ALIAS_TYPES),
|
||||
.whereIn('field', fieldKeys),
|
||||
]);
|
||||
|
||||
const existingFields = uniq([
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
"esModuleInterop": true,
|
||||
"target": "es6",
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": true,
|
||||
"outDir": "dist"
|
||||
"sourceMap": false,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"lib": [
|
||||
"es2015"
|
||||
]
|
||||
"es2019"
|
||||
],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user