mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
Insights 2.0 (#14096)
* query function added to list * dashboard reading query, adding to object * typecasting of filter vals needed still * numbers accepting strings too * json-to-graphql-query => devD * fixed unneeded return in list index.ts * stitching and calling but not actually calling * calls on panel change * query object += new panel before dashboard save * uuid generated in app not api * fixed panel ids in query * fixed the tests I just wrote * passing the query data down! * list showing data * objDiff test moved to test * metric bug fixes + data * dashboard logic * time series conversion started * timeseries GQL query almost there * query querying * chart loading * aggregate handling improved * error handling for aggregate+filter errors * removed query on empty queryObj * maybe more error handling * more error handling working * improvements to erorr handling * stitchGQL() error return type corrected * added string fields to COUNT * pushing up but needs work * not an endless recursion * its not pretty but it works. * throws an error * system collections supported * refactor to solve some errors * loading correct * metric function fixed * data loading but not blocking rendering * removed redundant code. * relational fields * deep nesting relations * options.precision has a default * relational fields fix. (thanks azri) * the limit * limit and time series * range has a default * datat to workspace * v-if * panels loading * workspaces dont get data anymore * package.json * requested changes * loading * get groups util * timeseries => script setup * list => script setup * metric => script setup * label => script setup * declare optional props * loadingPanels: only loading spinner on loading panels * remove unneeded parseDate!! * applyDataToPanels tests * -.only * remove unneeded steps * processQuery tests * tests * removed unused var * jest.config and some queryCaller tests * one more test * query tests * typo * clean up * fix some but not all bugs * bugs from merge fixed * Start cleaning up 🧹 * Refactor custom input type * Small tweaks in list index * Cleanup imports * Require Query object to be returned from query prop * Tweak return statement * Fix imports * Cleanup metric watch effect * Tweaks tweaks tweaks * Don't rely on options, simplify fetch logic * Add paths to validation errors * [WIP] Start handling things in the store * Rework query fetching logic into store * Clean up data passing * Use composition setup for insights store * Remove outdated * Fix missing return * Allow batch updating in REST API Allows sending an array of partial items to the endpoints, updating all to their own values * Add batch update to graphql * Start integrating edits * Readd clear * Add deletion * Add duplication * Finish create flow * Resolve cache refresh on panel config * Prevent warnings about component name * Improve loading state * Finalize dashboard overhaul * Add auto-refresh sidebar detail * Add efficient panel reloading * Set/remove errors on succeeded requests * Move options rendering to shared * Fix wrong imports, render options in app * Selectively reload panels with changed variables * Ensure newly added panels don't lose data * Only refresh panel if data query changed * Never use empty filter object in metric query * Add default value support to variable panel * Centralize no-data state * Only reload data on var change when query is altered * Fix build * Fix time series order * Remove unused utils * Remove no-longer-used logic * Mark batch update result as non-nullable in GraphQL schema * Interim flows fix * Skip parsing undefined keys * Refresh insights dashboard when discarding changes * Don't submit primary key when updating batch * Handle null prop field better * Tweak panel padding Co-authored-by: jaycammarano <jay.cammarano@gmail.com> Co-authored-by: Azri Kahar <42867097+azrikahar@users.noreply.github.com> Co-authored-by: ian <licitdev@gmail.com>
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import { BaseException } from '@directus/shared/exceptions';
|
||||
import { parseJSON } from '@directus/shared/utils';
|
||||
import { Router } from 'express';
|
||||
import flatten from 'flat';
|
||||
import jwt from 'jsonwebtoken';
|
||||
@@ -5,7 +7,6 @@ import ms from 'ms';
|
||||
import { Client, errors, generators, Issuer } from 'openid-client';
|
||||
import { getAuthProvider } from '../../auth';
|
||||
import env from '../../env';
|
||||
import { BaseException } from '@directus/shared/exceptions';
|
||||
import {
|
||||
InvalidConfigException,
|
||||
InvalidCredentialsException,
|
||||
@@ -19,7 +20,6 @@ import { AuthData, AuthDriverOptions, User } from '../../types';
|
||||
import asyncHandler from '../../utils/async-handler';
|
||||
import { getConfigFromEnv } from '../../utils/get-config-from-env';
|
||||
import { getIPFromReq } from '../../utils/get-ip-from-req';
|
||||
import { parseJSON } from '../../utils/parse-json';
|
||||
import { Url } from '../../utils/url';
|
||||
import { LocalAuthDriver } from './local';
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { BaseException } from '@directus/shared/exceptions';
|
||||
import { parseJSON } from '@directus/shared/utils';
|
||||
import { Router } from 'express';
|
||||
import flatten from 'flat';
|
||||
import jwt from 'jsonwebtoken';
|
||||
@@ -5,7 +7,6 @@ import ms from 'ms';
|
||||
import { Client, errors, generators, Issuer } from 'openid-client';
|
||||
import { getAuthProvider } from '../../auth';
|
||||
import env from '../../env';
|
||||
import { BaseException } from '@directus/shared/exceptions';
|
||||
import {
|
||||
InvalidConfigException,
|
||||
InvalidCredentialsException,
|
||||
@@ -19,7 +20,6 @@ import { AuthData, AuthDriverOptions, User } from '../../types';
|
||||
import asyncHandler from '../../utils/async-handler';
|
||||
import { getConfigFromEnv } from '../../utils/get-config-from-env';
|
||||
import { getIPFromReq } from '../../utils/get-ip-from-req';
|
||||
import { parseJSON } from '../../utils/parse-json';
|
||||
import { Url } from '../../utils/url';
|
||||
import { LocalAuthDriver } from './local';
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { parseJSON } from '@directus/shared/utils';
|
||||
import chalk from 'chalk';
|
||||
import { promises as fs } from 'fs';
|
||||
import inquirer from 'inquirer';
|
||||
@@ -10,7 +11,6 @@ import { Snapshot } from '../../../types';
|
||||
import { applySnapshot, isNestedMetaUpdate } from '../../../utils/apply-snapshot';
|
||||
import { getSnapshot } from '../../../utils/get-snapshot';
|
||||
import { getSnapshotDiff } from '../../../utils/get-snapshot-diff';
|
||||
import { parseJSON } from '../../../utils/parse-json';
|
||||
|
||||
export async function apply(snapshotPath: string, options?: { yes: boolean; dryRun: boolean }): Promise<void> {
|
||||
const filename = path.resolve(process.cwd(), snapshotPath);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Range } from '@directus/drive';
|
||||
import { parseJSON } from '@directus/shared/utils';
|
||||
import { Router } from 'express';
|
||||
import helmet from 'helmet';
|
||||
import { merge, pick } from 'lodash';
|
||||
@@ -12,7 +13,6 @@ import { AssetsService, PayloadService } from '../services';
|
||||
import { TransformationMethods, TransformationParams, TransformationPreset } from '../types/assets';
|
||||
import asyncHandler from '../utils/async-handler';
|
||||
import { getConfigFromEnv } from '../utils/get-config-from-env';
|
||||
import { parseJSON } from '../utils/parse-json';
|
||||
|
||||
const router = Router();
|
||||
|
||||
|
||||
@@ -97,7 +97,9 @@ router.patch(
|
||||
|
||||
let keys: PrimaryKey[] = [];
|
||||
|
||||
if (req.body.keys) {
|
||||
if (Array.isArray(req.body)) {
|
||||
keys = await service.updateBatch(req.body);
|
||||
} else if (req.body.keys) {
|
||||
keys = await service.updateMany(req.body.keys, req.body.data);
|
||||
} else {
|
||||
keys = await service.updateByQuery(req.body.query, req.body.data);
|
||||
|
||||
@@ -258,7 +258,9 @@ router.patch(
|
||||
|
||||
let keys: PrimaryKey[] = [];
|
||||
|
||||
if (req.body.keys) {
|
||||
if (Array.isArray(req.body)) {
|
||||
keys = await service.updateBatch(req.body);
|
||||
} else if (req.body.keys) {
|
||||
keys = await service.updateMany(req.body.keys, req.body.data);
|
||||
} else {
|
||||
keys = await service.updateByQuery(req.body.query, req.body.data);
|
||||
|
||||
@@ -124,7 +124,9 @@ router.patch(
|
||||
|
||||
let keys: PrimaryKey[] = [];
|
||||
|
||||
if (req.body.keys) {
|
||||
if (Array.isArray(req.body)) {
|
||||
keys = await service.updateBatch(req.body);
|
||||
} else if (req.body.keys) {
|
||||
keys = await service.updateMany(req.body.keys, req.body.data);
|
||||
} else {
|
||||
keys = await service.updateByQuery(req.body.query, req.body.data);
|
||||
|
||||
@@ -105,7 +105,9 @@ router.patch(
|
||||
|
||||
let keys: PrimaryKey[] = [];
|
||||
|
||||
if (req.body.keys) {
|
||||
if (Array.isArray(req.body)) {
|
||||
keys = await service.updateBatch(req.body);
|
||||
} else if (req.body.keys) {
|
||||
keys = await service.updateMany(req.body.keys, req.body.data);
|
||||
} else {
|
||||
keys = await service.updateByQuery(req.body.query, req.body.data);
|
||||
|
||||
@@ -135,7 +135,9 @@ router.patch(
|
||||
|
||||
let keys: PrimaryKey[] = [];
|
||||
|
||||
if (req.body.keys) {
|
||||
if (Array.isArray(req.body)) {
|
||||
keys = await service.updateBatch(req.body);
|
||||
} else if (req.body.keys) {
|
||||
keys = await service.updateMany(req.body.keys, req.body.data);
|
||||
} else {
|
||||
keys = await service.updateByQuery(req.body.query, req.body.data);
|
||||
|
||||
@@ -106,7 +106,9 @@ router.patch(
|
||||
|
||||
let keys: PrimaryKey[] = [];
|
||||
|
||||
if (req.body.keys) {
|
||||
if (Array.isArray(req.body)) {
|
||||
keys = await service.updateBatch(req.body);
|
||||
} else if (req.body.keys) {
|
||||
keys = await service.updateMany(req.body.keys, req.body.data);
|
||||
} else {
|
||||
keys = await service.updateByQuery(req.body.query, req.body.data);
|
||||
|
||||
@@ -97,7 +97,9 @@ router.patch(
|
||||
|
||||
let keys: PrimaryKey[] = [];
|
||||
|
||||
if (req.body.keys) {
|
||||
if (Array.isArray(req.body)) {
|
||||
keys = await service.updateBatch(req.body);
|
||||
} else if (req.body.keys) {
|
||||
keys = await service.updateMany(req.body.keys, req.body.data);
|
||||
} else {
|
||||
keys = await service.updateByQuery(req.body.query, req.body.data);
|
||||
|
||||
@@ -97,7 +97,9 @@ router.patch(
|
||||
|
||||
let keys: PrimaryKey[] = [];
|
||||
|
||||
if (req.body.keys) {
|
||||
if (Array.isArray(req.body)) {
|
||||
keys = await service.updateBatch(req.body);
|
||||
} else if (req.body.keys) {
|
||||
keys = await service.updateMany(req.body.keys, req.body.data);
|
||||
} else {
|
||||
keys = await service.updateByQuery(req.body.query, req.body.data);
|
||||
|
||||
@@ -107,7 +107,9 @@ router.patch(
|
||||
|
||||
let keys: PrimaryKey[] = [];
|
||||
|
||||
if (req.body.keys) {
|
||||
if (Array.isArray(req.body)) {
|
||||
keys = await service.updateBatch(req.body);
|
||||
} else if (req.body.keys) {
|
||||
keys = await service.updateMany(req.body.keys, req.body.data);
|
||||
} else {
|
||||
keys = await service.updateByQuery(req.body.query, req.body.data);
|
||||
|
||||
@@ -106,7 +106,9 @@ router.patch(
|
||||
|
||||
let keys: PrimaryKey[] = [];
|
||||
|
||||
if (req.body.keys) {
|
||||
if (Array.isArray(req.body)) {
|
||||
keys = await service.updateBatch(req.body);
|
||||
} else if (req.body.keys) {
|
||||
keys = await service.updateMany(req.body.keys, req.body.data);
|
||||
} else {
|
||||
keys = await service.updateByQuery(req.body.query, req.body.data);
|
||||
|
||||
@@ -98,7 +98,9 @@ router.patch(
|
||||
|
||||
let keys: PrimaryKey[] = [];
|
||||
|
||||
if (req.body.keys) {
|
||||
if (Array.isArray(req.body)) {
|
||||
keys = await service.updateBatch(req.body);
|
||||
} else if (req.body.keys) {
|
||||
keys = await service.updateMany(req.body.keys, req.body.data);
|
||||
} else {
|
||||
keys = await service.updateByQuery(req.body.query, req.body.data);
|
||||
|
||||
@@ -200,7 +200,9 @@ router.patch(
|
||||
|
||||
let keys: PrimaryKey[] = [];
|
||||
|
||||
if (req.body.keys) {
|
||||
if (Array.isArray(req.body)) {
|
||||
keys = await service.updateBatch(req.body);
|
||||
} else if (req.body.keys) {
|
||||
keys = await service.updateMany(req.body.keys, req.body.data);
|
||||
} else {
|
||||
keys = await service.updateByQuery(req.body.query, req.body.data);
|
||||
|
||||
@@ -183,7 +183,9 @@ router.patch(
|
||||
|
||||
let keys: PrimaryKey[] = [];
|
||||
|
||||
if (req.body.keys) {
|
||||
if (Array.isArray(req.body)) {
|
||||
keys = await service.updateBatch(req.body);
|
||||
} else if (req.body.keys) {
|
||||
keys = await service.updateMany(req.body.keys, req.body.data);
|
||||
} else {
|
||||
keys = await service.updateByQuery(req.body.query, req.body.data);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { parseJSON } from '@directus/shared/utils';
|
||||
import { Knex } from 'knex';
|
||||
import { parseJSON } from '../../utils/parse-json';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable('directus_relations', (table) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { parseJSON } from '@directus/shared/utils';
|
||||
import { Knex } from 'knex';
|
||||
import { parseJSON } from '../../utils/parse-json';
|
||||
|
||||
// [before, after, after-option additions]
|
||||
const changes: [string, string, Record<string, any>?][] = [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { parseJSON } from '@directus/shared/utils';
|
||||
import { Knex } from 'knex';
|
||||
import logger from '../../logger';
|
||||
import { parseJSON } from '../../utils/parse-json';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const dividerGroups = await knex.select('*').from('directus_fields').where('interface', '=', 'group-divider');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { parseJSON } from '@directus/shared/utils';
|
||||
import { Knex } from 'knex';
|
||||
import { parseJSON } from '../../utils/parse-json';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const groups = await knex.select('*').from('directus_fields').where({ interface: 'group-standard' });
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { parseJSON } from '@directus/shared/utils';
|
||||
import { Knex } from 'knex';
|
||||
import { parseJSON } from '../../utils/parse-json';
|
||||
|
||||
// Change image metadata structure to match the output from 'exifr'
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Filter, LogicalFilterAND } from '@directus/shared/types';
|
||||
import { parseJSON } from '@directus/shared/utils';
|
||||
import { Knex } from 'knex';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { parseJSON } from '../../utils/parse-json';
|
||||
|
||||
type OldFilter = {
|
||||
key: string;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { parseJSON, toArray } from '@directus/shared/utils';
|
||||
import { Knex } from 'knex';
|
||||
import { toArray } from '@directus/shared/utils';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { parseJSON } from '../../utils/parse-json';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.createTable('directus_flows', (table) => {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { clone, toNumber, toString } from 'lodash';
|
||||
import path from 'path';
|
||||
import { requireYAML } from './utils/require-yaml';
|
||||
import { toArray } from '@directus/shared/utils';
|
||||
import { parseJSON } from './utils/parse-json';
|
||||
import { parseJSON } from '@directus/shared/utils';
|
||||
|
||||
// keeping this here for now to prevent a circular import to constants.ts
|
||||
const allowedEnvironmentVars = [
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import * as sharedExceptions from '@directus/shared/exceptions';
|
||||
import {
|
||||
Accountability,
|
||||
Action,
|
||||
ActionHandler,
|
||||
FilterHandler,
|
||||
Flow,
|
||||
Operation,
|
||||
OperationHandler,
|
||||
SchemaOverview,
|
||||
Accountability,
|
||||
Action,
|
||||
} from '@directus/shared/types';
|
||||
import { applyOptionsData } from '@directus/shared/utils';
|
||||
import fastRedact from 'fast-redact';
|
||||
import { Knex } from 'knex';
|
||||
import { omit } from 'lodash';
|
||||
import { get } from 'micromustache';
|
||||
import { schedule, validate } from 'node-cron';
|
||||
import getDatabase from './database';
|
||||
@@ -16,18 +20,14 @@ import emitter from './emitter';
|
||||
import env from './env';
|
||||
import * as exceptions from './exceptions';
|
||||
import logger from './logger';
|
||||
import { getMessenger } from './messenger';
|
||||
import * as services from './services';
|
||||
import { FlowsService } from './services';
|
||||
import { ActivityService } from './services/activity';
|
||||
import { RevisionsService } from './services/revisions';
|
||||
import { EventHandler } from './types';
|
||||
import { constructFlowTree } from './utils/construct-flow-tree';
|
||||
import { getSchema } from './utils/get-schema';
|
||||
import { ActivityService } from './services/activity';
|
||||
import { RevisionsService } from './services/revisions';
|
||||
import { Knex } from 'knex';
|
||||
import { omit } from 'lodash';
|
||||
import { getMessenger } from './messenger';
|
||||
import fastRedact from 'fast-redact';
|
||||
import { applyOperationOptions } from './utils/operation-options';
|
||||
import { JobQueue } from './utils/job-queue';
|
||||
|
||||
let flowManager: FlowManager | undefined;
|
||||
@@ -376,7 +376,7 @@ class FlowManager {
|
||||
|
||||
const handler = this.operations[operation.type];
|
||||
|
||||
const options = applyOperationOptions(operation.options, keyedData);
|
||||
const options = applyOptionsData(operation.options, keyedData);
|
||||
|
||||
try {
|
||||
const result = await handler(options, {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { parseJSON } from '@directus/shared/utils';
|
||||
import IORedis from 'ioredis';
|
||||
import env from './env';
|
||||
import { getConfigFromEnv } from './utils/get-config-from-env';
|
||||
import { parseJSON } from './utils/parse-json';
|
||||
|
||||
export type MessengerSubscriptionCallback = (payload: Record<string, any>) => void;
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { parseJSON } from '@directus/shared/utils';
|
||||
import { RequestHandler } from 'express';
|
||||
import { DocumentNode, getOperationAST, parse, Source } from 'graphql';
|
||||
import { InvalidPayloadException, InvalidQueryException, MethodNotAllowedException } from '../exceptions';
|
||||
import { GraphQLParams } from '../types';
|
||||
import asyncHandler from '../utils/async-handler';
|
||||
import { parseJSON } from '../utils/parse-json';
|
||||
|
||||
export const parseGraphQL: RequestHandler = asyncHandler(async (req, res, next) => {
|
||||
if (req.method !== 'GET' && req.method !== 'POST') {
|
||||
|
||||
@@ -27,8 +27,10 @@ export const validateBatch = (scope: 'read' | 'update' | 'delete'): RequestHandl
|
||||
batchSchema = batchSchema.xor('query', 'keys');
|
||||
}
|
||||
|
||||
// In updates, we add a required `data` that holds the update payload
|
||||
// In updates, we add a required `data` that holds the update payload if an array isn't used
|
||||
if (scope === 'update') {
|
||||
if (Array.isArray(req.body)) return next();
|
||||
|
||||
batchSchema = batchSchema.keys({
|
||||
data: Joi.object().unknown().required(),
|
||||
});
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Accountability, PrimaryKey } from '@directus/shared/types';
|
||||
import { defineOperationApi, toArray } from '@directus/shared/utils';
|
||||
import { defineOperationApi, optionToObject, toArray } from '@directus/shared/utils';
|
||||
import { ItemsService } from '../../services';
|
||||
import { Item } from '../../types';
|
||||
import { optionToObject } from '../../utils/operation-options';
|
||||
import { getAccountabilityForRole } from '../../utils/get-accountability-for-role';
|
||||
|
||||
type Options = {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Accountability, PrimaryKey } from '@directus/shared/types';
|
||||
import { defineOperationApi, toArray } from '@directus/shared/utils';
|
||||
import { defineOperationApi, optionToObject, toArray } from '@directus/shared/utils';
|
||||
import { ItemsService } from '../../services';
|
||||
import { getAccountabilityForRole } from '../../utils/get-accountability-for-role';
|
||||
import { optionToObject } from '../../utils/operation-options';
|
||||
import { sanitizeQuery } from '../../utils/sanitize-query';
|
||||
|
||||
type Options = {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Accountability, PrimaryKey } from '@directus/shared/types';
|
||||
import { defineOperationApi, toArray } from '@directus/shared/utils';
|
||||
import { defineOperationApi, optionToObject, toArray } from '@directus/shared/utils';
|
||||
import { ItemsService } from '../../services';
|
||||
import { Item } from '../../types';
|
||||
import { getAccountabilityForRole } from '../../utils/get-accountability-for-role';
|
||||
import { optionToObject } from '../../utils/operation-options';
|
||||
import { sanitizeQuery } from '../../utils/sanitize-query';
|
||||
|
||||
type Options = {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Accountability, PrimaryKey } from '@directus/shared/types';
|
||||
import { defineOperationApi, toArray } from '@directus/shared/utils';
|
||||
import { defineOperationApi, optionToObject, toArray } from '@directus/shared/utils';
|
||||
import { ItemsService } from '../../services';
|
||||
import { Item } from '../../types';
|
||||
import { getAccountabilityForRole } from '../../utils/get-accountability-for-role';
|
||||
import { optionToObject } from '../../utils/operation-options';
|
||||
import { sanitizeQuery } from '../../utils/sanitize-query';
|
||||
|
||||
type Options = {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { defineOperationApi } from '@directus/shared/utils';
|
||||
import { defineOperationApi, optionToString } from '@directus/shared/utils';
|
||||
import logger from '../../logger';
|
||||
import { optionToString } from '../../utils/operation-options';
|
||||
|
||||
type Options = {
|
||||
message: unknown;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Accountability } from '@directus/shared/types';
|
||||
import { defineOperationApi } from '@directus/shared/utils';
|
||||
import { defineOperationApi, optionToString } from '@directus/shared/utils';
|
||||
import { NotificationsService } from '../../services';
|
||||
import { optionToString } from '../../utils/operation-options';
|
||||
import { getAccountabilityForRole } from '../../utils/get-accountability-for-role';
|
||||
|
||||
type Options = {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { defineOperationApi } from '@directus/shared/utils';
|
||||
import { parseJSON } from '../../utils/parse-json';
|
||||
import { defineOperationApi, parseJSON } from '@directus/shared/utils';
|
||||
|
||||
type Options = {
|
||||
json: string;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { defineOperationApi } from '@directus/shared/utils';
|
||||
import { defineOperationApi, optionToObject } from '@directus/shared/utils';
|
||||
import { getFlowManager } from '../../flows';
|
||||
import { optionToObject } from '../../utils/operation-options';
|
||||
|
||||
type Options = {
|
||||
flow: string;
|
||||
|
||||
@@ -35,6 +35,15 @@ export class FlowsService extends ItemsService<FlowRaw> {
|
||||
return result;
|
||||
}
|
||||
|
||||
async updateBatch(data: Partial<Item>[], opts?: MutationOptions): Promise<PrimaryKey[]> {
|
||||
const flowManager = getFlowManager();
|
||||
|
||||
const result = await super.updateBatch(data, opts);
|
||||
await flowManager.reload();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async updateMany(keys: PrimaryKey[], data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey[]> {
|
||||
const flowManager = getFlowManager();
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
GraphQLUnionType,
|
||||
InlineFragmentNode,
|
||||
IntValueNode,
|
||||
NoSchemaIntrospectionCustomRule,
|
||||
ObjectFieldNode,
|
||||
ObjectValueNode,
|
||||
SelectionNode,
|
||||
@@ -46,71 +47,56 @@ import {
|
||||
import { Knex } from 'knex';
|
||||
import { flatten, get, isObject, mapKeys, merge, omit, pick, set, transform, uniq } from 'lodash';
|
||||
import ms from 'ms';
|
||||
import { clearSystemCache, getCache } from '../cache';
|
||||
import { DEFAULT_AUTH_PROVIDER } from '../constants';
|
||||
import getDatabase from '../database';
|
||||
import env from '../env';
|
||||
import { ForbiddenException, GraphQLValidationException, InvalidPayloadException } from '../exceptions';
|
||||
import { getExtensionManager } from '../extensions';
|
||||
import { AbstractServiceOptions, GraphQLParams, Item } from '../types';
|
||||
import { generateHash } from '../utils/generate-hash';
|
||||
import { getGraphQLType } from '../utils/get-graphql-type';
|
||||
import { reduceSchema } from '../utils/reduce-schema';
|
||||
import { sanitizeQuery } from '../utils/sanitize-query';
|
||||
import { validateQuery } from '../utils/validate-query';
|
||||
import { ActivityService } from './activity';
|
||||
import { AuthenticationService } from './authentication';
|
||||
import { CollectionsService } from './collections';
|
||||
import { FieldsService } from './fields';
|
||||
import { FilesService } from './files';
|
||||
import { FlowsService } from './flows';
|
||||
import { FoldersService } from './folders';
|
||||
import { ItemsService } from './items';
|
||||
import { NotificationsService } from './notifications';
|
||||
import { OperationsService } from './operations';
|
||||
import { PermissionsService } from './permissions';
|
||||
import { PresetsService } from './presets';
|
||||
import { RelationsService } from './relations';
|
||||
import { RevisionsService } from './revisions';
|
||||
import { RolesService } from './roles';
|
||||
import { ServerService } from './server';
|
||||
import { SettingsService } from './settings';
|
||||
import { SharesService } from './shares';
|
||||
import { SpecificationService } from './specifications';
|
||||
import { TFAService } from './tfa';
|
||||
import { UsersService } from './users';
|
||||
import { UtilsService } from './utils';
|
||||
import { WebhooksService } from './webhooks';
|
||||
import { clearSystemCache, getCache } from '../../cache';
|
||||
import { DEFAULT_AUTH_PROVIDER } from '../../constants';
|
||||
import getDatabase from '../../database';
|
||||
import env from '../../env';
|
||||
import { ForbiddenException, GraphQLValidationException, InvalidPayloadException } from '../../exceptions';
|
||||
import { getExtensionManager } from '../../extensions';
|
||||
import { AbstractServiceOptions, GraphQLParams, Item } from '../../types';
|
||||
import { generateHash } from '../../utils/generate-hash';
|
||||
import { getGraphQLType } from '../../utils/get-graphql-type';
|
||||
import { reduceSchema } from '../../utils/reduce-schema';
|
||||
import { sanitizeQuery } from '../../utils/sanitize-query';
|
||||
import { validateQuery } from '../../utils/validate-query';
|
||||
import { ActivityService } from '../activity';
|
||||
import { AuthenticationService } from '../authentication';
|
||||
import { CollectionsService } from '../collections';
|
||||
import { FieldsService } from '../fields';
|
||||
import { FilesService } from '../files';
|
||||
import { FlowsService } from '../flows';
|
||||
import { FoldersService } from '../folders';
|
||||
import { ItemsService } from '../items';
|
||||
import { NotificationsService } from '../notifications';
|
||||
import { OperationsService } from '../operations';
|
||||
import { PermissionsService } from '../permissions';
|
||||
import { PresetsService } from '../presets';
|
||||
import { RelationsService } from '../relations';
|
||||
import { RevisionsService } from '../revisions';
|
||||
import { RolesService } from '../roles';
|
||||
import { ServerService } from '../server';
|
||||
import { SettingsService } from '../settings';
|
||||
import { SharesService } from '../shares';
|
||||
import { SpecificationService } from '../specifications';
|
||||
import { TFAService } from '../tfa';
|
||||
import { UsersService } from '../users';
|
||||
import { UtilsService } from '../utils';
|
||||
import { WebhooksService } from '../webhooks';
|
||||
|
||||
const GraphQLVoid = new GraphQLScalarType({
|
||||
name: 'Void',
|
||||
import { GraphQLDate } from './types/date';
|
||||
import { GraphQLGeoJSON } from './types/geojson';
|
||||
import { GraphQLStringOrFloat } from './types/string-or-float';
|
||||
import { GraphQLVoid } from './types/void';
|
||||
|
||||
description: 'Represents NULL values',
|
||||
import { PrimaryKey } from '@directus/shared/types';
|
||||
|
||||
serialize() {
|
||||
return null;
|
||||
},
|
||||
import { addPathToValidationError } from './utils/add-path-to-validation-error';
|
||||
|
||||
parseValue() {
|
||||
return null;
|
||||
},
|
||||
const validationRules = Array.from(specifiedRules);
|
||||
|
||||
parseLiteral() {
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
export const GraphQLGeoJSON = new GraphQLScalarType({
|
||||
...GraphQLJSON,
|
||||
name: 'GraphQLGeoJSON',
|
||||
description: 'GeoJSON value',
|
||||
});
|
||||
|
||||
export const GraphQLDate = new GraphQLScalarType({
|
||||
...GraphQLString,
|
||||
name: 'Date',
|
||||
description: 'ISO8601 Date values',
|
||||
});
|
||||
if (env.GRAPHQL_INTROSPECTION === false) {
|
||||
validationRules.push(NoSchemaIntrospectionCustomRule);
|
||||
}
|
||||
|
||||
/**
|
||||
* These should be ignored in the context of GraphQL, and/or are replaced by a custom resolver (for non-standard structures)
|
||||
@@ -122,6 +108,7 @@ const SYSTEM_DENY_LIST = [
|
||||
'directus_migrations',
|
||||
'directus_sessions',
|
||||
];
|
||||
|
||||
const READ_ONLY = ['directus_activity', 'directus_revisions'];
|
||||
|
||||
export class GraphQLService {
|
||||
@@ -148,18 +135,9 @@ export class GraphQLService {
|
||||
}: GraphQLParams): Promise<FormattedExecutionResult> {
|
||||
const schema = this.getSchema();
|
||||
|
||||
const validationErrors = validate(schema, document, [
|
||||
...specifiedRules,
|
||||
(context) => ({
|
||||
Field(node) {
|
||||
if (env.GRAPHQL_INTROSPECTION === false && (node.name.value === '__schema' || node.name.value === '__type')) {
|
||||
context.reportError(
|
||||
new GraphQLError('GraphQL introspection is not allowed. The query contained __schema or __type.', [node])
|
||||
);
|
||||
}
|
||||
},
|
||||
}),
|
||||
]);
|
||||
const validationErrors = validate(schema, document, validationRules).map((validationError) =>
|
||||
addPathToValidationError(validationError)
|
||||
);
|
||||
|
||||
if (validationErrors.length > 0) {
|
||||
throw new GraphQLValidationException({ graphqlErrors: validationErrors });
|
||||
@@ -312,6 +290,10 @@ export class GraphQLService {
|
||||
`update_${collection.collection}_items`
|
||||
);
|
||||
|
||||
acc[`update_${collectionName}_batch`] = UpdateCollectionTypes[collection.collection].getResolver(
|
||||
`update_${collection.collection}_batch`
|
||||
);
|
||||
|
||||
acc[`update_${collectionName}_item`] = UpdateCollectionTypes[collection.collection].getResolver(
|
||||
`update_${collection.collection}_item`
|
||||
);
|
||||
@@ -653,32 +635,33 @@ export class GraphQLService {
|
||||
},
|
||||
});
|
||||
|
||||
// Uses StringOrFloat rather than Float to support api dynamic variables (like `$NOW`)
|
||||
const NumberFilterOperators = schemaComposer.createInputTC({
|
||||
name: 'number_filter_operators',
|
||||
fields: {
|
||||
_eq: {
|
||||
type: GraphQLFloat,
|
||||
type: GraphQLStringOrFloat,
|
||||
},
|
||||
_neq: {
|
||||
type: GraphQLFloat,
|
||||
type: GraphQLStringOrFloat,
|
||||
},
|
||||
_in: {
|
||||
type: new GraphQLList(GraphQLFloat),
|
||||
type: new GraphQLList(GraphQLStringOrFloat),
|
||||
},
|
||||
_nin: {
|
||||
type: new GraphQLList(GraphQLFloat),
|
||||
type: new GraphQLList(GraphQLStringOrFloat),
|
||||
},
|
||||
_gt: {
|
||||
type: GraphQLFloat,
|
||||
type: GraphQLStringOrFloat,
|
||||
},
|
||||
_gte: {
|
||||
type: GraphQLFloat,
|
||||
type: GraphQLStringOrFloat,
|
||||
},
|
||||
_lt: {
|
||||
type: GraphQLFloat,
|
||||
type: GraphQLStringOrFloat,
|
||||
},
|
||||
_lte: {
|
||||
type: GraphQLFloat,
|
||||
type: GraphQLStringOrFloat,
|
||||
},
|
||||
_null: {
|
||||
type: GraphQLBoolean,
|
||||
@@ -1143,6 +1126,27 @@ export class GraphQLService {
|
||||
await self.resolveMutation(args, info),
|
||||
});
|
||||
} else {
|
||||
UpdateCollectionTypes[collection.collection].addResolver({
|
||||
name: `update_${collection.collection}_batch`,
|
||||
type: collectionIsReadable
|
||||
? new GraphQLNonNull(
|
||||
new GraphQLList(new GraphQLNonNull(ReadCollectionTypes[collection.collection].getType()))
|
||||
)
|
||||
: GraphQLBoolean,
|
||||
args: {
|
||||
...(collectionIsReadable
|
||||
? ReadCollectionTypes[collection.collection].getResolver(collection.collection).getArgs()
|
||||
: {}),
|
||||
data: [
|
||||
toInputObjectType(UpdateCollectionTypes[collection.collection]).setTypeName(
|
||||
`update_${collection.collection}_input`
|
||||
).NonNull,
|
||||
],
|
||||
},
|
||||
resolve: async ({ args, info }: { args: Record<string, any>; info: GraphQLResolveInfo }) =>
|
||||
await self.resolveMutation(args, info),
|
||||
});
|
||||
|
||||
UpdateCollectionTypes[collection.collection].addResolver({
|
||||
name: `update_${collection.collection}_items`,
|
||||
type: collectionIsReadable
|
||||
@@ -1290,12 +1294,15 @@ export class GraphQLService {
|
||||
const query = this.getQuery(args, selections || [], info.variableValues);
|
||||
|
||||
const singleton =
|
||||
collection.endsWith('_batch') === false &&
|
||||
collection.endsWith('_items') === false &&
|
||||
collection.endsWith('_item') === false &&
|
||||
collection in this.schema.collections;
|
||||
|
||||
const single = collection.endsWith('_items') === false;
|
||||
const single = collection.endsWith('_items') === false && collection.endsWith('_batch') === false;
|
||||
const batchUpdate = action === 'update' && collection.endsWith('_batch');
|
||||
|
||||
if (collection.endsWith('_batch')) collection = collection.slice(0, -6);
|
||||
if (collection.endsWith('_items')) collection = collection.slice(0, -6);
|
||||
if (collection.endsWith('_item')) collection = collection.slice(0, -5);
|
||||
|
||||
@@ -1329,7 +1336,14 @@ export class GraphQLService {
|
||||
}
|
||||
|
||||
if (action === 'update') {
|
||||
const keys = await service.updateMany(args.ids, args.data);
|
||||
const keys: PrimaryKey[] = [];
|
||||
|
||||
if (batchUpdate) {
|
||||
keys.push(...(await service.updateBatch(args.data)));
|
||||
} else {
|
||||
keys.push(...(await service.updateMany(args.ids, args.data)));
|
||||
}
|
||||
|
||||
return hasQuery ? await service.readMany(keys, query) : true;
|
||||
}
|
||||
|
||||
7
api/src/services/graphql/types/date.ts
Normal file
7
api/src/services/graphql/types/date.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { GraphQLString, GraphQLScalarType } from 'graphql';
|
||||
|
||||
export const GraphQLDate = new GraphQLScalarType({
|
||||
...GraphQLString,
|
||||
name: 'Date',
|
||||
description: 'ISO8601 Date values',
|
||||
});
|
||||
8
api/src/services/graphql/types/geojson.ts
Normal file
8
api/src/services/graphql/types/geojson.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { GraphQLScalarType } from 'graphql';
|
||||
import { GraphQLJSON } from 'graphql-compose';
|
||||
|
||||
export const GraphQLGeoJSON = new GraphQLScalarType({
|
||||
...GraphQLJSON,
|
||||
name: 'GraphQLGeoJSON',
|
||||
description: 'GeoJSON value',
|
||||
});
|
||||
35
api/src/services/graphql/types/string-or-float.ts
Normal file
35
api/src/services/graphql/types/string-or-float.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { GraphQLScalarType, Kind } from 'graphql';
|
||||
|
||||
/**
|
||||
* Adopted from https://kamranicus.com/handling-multiple-scalar-types-in-graphql/
|
||||
*/
|
||||
|
||||
export const GraphQLStringOrFloat = new GraphQLScalarType({
|
||||
name: 'GraphQLStringOrFloat',
|
||||
description: 'A Float or a String',
|
||||
serialize(value) {
|
||||
if (typeof value !== 'string' && typeof value !== 'number') {
|
||||
throw new Error('Value must be either a String or a Float');
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
parseValue(value) {
|
||||
if (typeof value !== 'string' && typeof value !== 'number') {
|
||||
throw new Error('Value must be either a String or a Float');
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
parseLiteral(ast) {
|
||||
switch (ast.kind) {
|
||||
case Kind.INT:
|
||||
case Kind.FLOAT:
|
||||
return Number(ast.value);
|
||||
case Kind.STRING:
|
||||
return ast.value;
|
||||
default:
|
||||
throw new Error('Value must be either a String or a Float');
|
||||
}
|
||||
},
|
||||
});
|
||||
19
api/src/services/graphql/types/void.ts
Normal file
19
api/src/services/graphql/types/void.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { GraphQLScalarType } from 'graphql';
|
||||
|
||||
export const GraphQLVoid = new GraphQLScalarType({
|
||||
name: 'Void',
|
||||
|
||||
description: 'Represents NULL values',
|
||||
|
||||
serialize() {
|
||||
return null;
|
||||
},
|
||||
|
||||
parseValue() {
|
||||
return null;
|
||||
},
|
||||
|
||||
parseLiteral() {
|
||||
return null;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import { GraphQLError, Token, locatedError } from 'graphql';
|
||||
|
||||
export function addPathToValidationError(validationError: GraphQLError): GraphQLError {
|
||||
const token = validationError.nodes?.[0]?.loc?.startToken;
|
||||
|
||||
if (!token) return validationError;
|
||||
|
||||
let prev: Token | null = token;
|
||||
|
||||
const queryRegex = /query_[A-Za-z0-9]{8}/;
|
||||
|
||||
while (prev) {
|
||||
if (prev.kind === 'Name' && prev.value && queryRegex.test(prev.value)) {
|
||||
return locatedError(validationError, validationError.nodes, [prev.value]);
|
||||
}
|
||||
|
||||
prev = prev.prev;
|
||||
}
|
||||
|
||||
return locatedError(validationError, validationError.nodes);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Accountability, Query, SchemaOverview } from '@directus/shared/types';
|
||||
import { toArray } from '@directus/shared/utils';
|
||||
import { parseJSON, toArray } from '@directus/shared/utils';
|
||||
import { queue } from 'async';
|
||||
import csv from 'csv-parser';
|
||||
import destroyStream from 'destroy';
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
import logger from '../logger';
|
||||
import { AbstractServiceOptions, File } from '../types';
|
||||
import { getDateFormatted } from '../utils/get-date-formatted';
|
||||
import { parseJSON } from '../utils/parse-json';
|
||||
import { FilesService } from './files';
|
||||
import { ItemsService } from './items';
|
||||
import { NotificationsService } from './notifications';
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { Accountability, Action, PermissionsAction, Query, SchemaOverview } from '@directus/shared/types';
|
||||
import Keyv from 'keyv';
|
||||
import { Knex } from 'knex';
|
||||
import { assign, clone, cloneDeep, pick, without } from 'lodash';
|
||||
import { assign, clone, cloneDeep, omit, pick, without } from 'lodash';
|
||||
import { getCache } from '../cache';
|
||||
import getDatabase from '../database';
|
||||
import runAST from '../database/run-ast';
|
||||
import emitter from '../emitter';
|
||||
import env from '../env';
|
||||
import { ForbiddenException } from '../exceptions';
|
||||
import { ForbiddenException, InvalidPayloadException } from '../exceptions';
|
||||
import { translateDatabaseError } from '../exceptions/database/translate';
|
||||
import { AbstractService, AbstractServiceOptions, Item as AnyItem, MutationOptions, PrimaryKey } from '../types';
|
||||
import getASTFromQuery from '../utils/get-ast-from-query';
|
||||
import { validateKeys } from '../utils/validate-keys';
|
||||
import { AuthorizationService } from './authorization';
|
||||
import { ActivityService, RevisionsService } from './index';
|
||||
import { PayloadService } from './payload';
|
||||
import { validateKeys } from '../utils/validate-keys';
|
||||
|
||||
export type QueryOptions = {
|
||||
stripNonRequested?: boolean;
|
||||
@@ -371,7 +371,31 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
}
|
||||
|
||||
/**
|
||||
* Update many items by primary key
|
||||
* Update multiple items in a single transaction
|
||||
*/
|
||||
async updateBatch(data: Partial<Item>[], opts?: MutationOptions): Promise<PrimaryKey[]> {
|
||||
const primaryKeyField = this.schema.collections[this.collection].primary;
|
||||
|
||||
const keys: PrimaryKey[] = [];
|
||||
|
||||
await this.knex.transaction(async (trx) => {
|
||||
const service = new ItemsService(this.collection, {
|
||||
accountability: this.accountability,
|
||||
knex: trx,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
for (const item of data) {
|
||||
if (!item[primaryKeyField]) throw new InvalidPayloadException(`Item in update misses primary key.`);
|
||||
keys.push(await service.updateOne(item[primaryKeyField]!, omit(item, primaryKeyField), opts));
|
||||
}
|
||||
});
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update many items by primary key, setting all items to the same change
|
||||
*/
|
||||
async updateMany(keys: PrimaryKey[], data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey[]> {
|
||||
const primaryKeyField = this.schema.collections[this.collection].primary;
|
||||
|
||||
@@ -35,6 +35,15 @@ export class OperationsService extends ItemsService<OperationRaw> {
|
||||
return result;
|
||||
}
|
||||
|
||||
async updateBatch(data: Partial<Item>[], opts?: MutationOptions): Promise<PrimaryKey[]> {
|
||||
const flowManager = getFlowManager();
|
||||
|
||||
const result = await super.updateBatch(data, opts);
|
||||
await flowManager.reload();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async updateMany(keys: PrimaryKey[], data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey[]> {
|
||||
const flowManager = getFlowManager();
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Accountability, Query, SchemaOverview } from '@directus/shared/types';
|
||||
import { toArray } from '@directus/shared/utils';
|
||||
import { parseJSON, toArray } from '@directus/shared/utils';
|
||||
import { format } from 'date-fns';
|
||||
import { unflatten } from 'flat';
|
||||
import Joi from 'joi';
|
||||
@@ -12,7 +12,6 @@ import { getHelpers, Helpers } from '../database/helpers';
|
||||
import { ForbiddenException, InvalidPayloadException } from '../exceptions';
|
||||
import { AbstractServiceOptions, Alterations, Item, PrimaryKey } from '../types';
|
||||
import { generateHash } from '../utils/generate-hash';
|
||||
import { parseJSON } from '../utils/parse-json';
|
||||
import { ItemsService } from './items';
|
||||
|
||||
type Action = 'create' | 'read' | 'update';
|
||||
|
||||
@@ -102,6 +102,12 @@ export class PermissionsService extends ItemsService {
|
||||
return res;
|
||||
}
|
||||
|
||||
async updateBatch(data: Partial<Item>[], opts?: MutationOptions) {
|
||||
const res = await super.updateBatch(data, opts);
|
||||
await clearSystemCache();
|
||||
return res;
|
||||
}
|
||||
|
||||
async updateMany(keys: PrimaryKey[], data: Partial<Item>, opts?: MutationOptions) {
|
||||
const res = await super.updateMany(keys, data, opts);
|
||||
await clearSystemCache();
|
||||
|
||||
@@ -79,6 +79,19 @@ export class RolesService extends ItemsService {
|
||||
return super.updateOne(key, data, opts);
|
||||
}
|
||||
|
||||
async updateBatch(data: Record<string, any>[], opts?: MutationOptions): Promise<PrimaryKey[]> {
|
||||
const primaryKeyField = this.schema.collections[this.collection].primary;
|
||||
|
||||
const keys = data.map((item) => item[primaryKeyField]);
|
||||
const setsToNoAdmin = data.some((item) => item.admin_access === false);
|
||||
|
||||
if (setsToNoAdmin) {
|
||||
await this.checkForOtherAdminRoles(keys);
|
||||
}
|
||||
|
||||
return super.updateBatch(data, opts);
|
||||
}
|
||||
|
||||
async updateMany(keys: PrimaryKey[], data: Record<string, any>, opts?: MutationOptions): Promise<PrimaryKey[]> {
|
||||
if ('admin_access' in data && data.admin_access === false) {
|
||||
await this.checkForOtherAdminRoles(keys);
|
||||
|
||||
@@ -182,6 +182,27 @@ export class UsersService extends ItemsService {
|
||||
return key;
|
||||
}
|
||||
|
||||
async updateBatch(data: Partial<Item>[], opts?: MutationOptions): Promise<PrimaryKey[]> {
|
||||
const primaryKeyField = this.schema.collections[this.collection].primary;
|
||||
|
||||
const keys: PrimaryKey[] = [];
|
||||
|
||||
await this.knex.transaction(async (trx) => {
|
||||
const service = new UsersService({
|
||||
accountability: this.accountability,
|
||||
knex: trx,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
for (const item of data) {
|
||||
if (!item[primaryKeyField]) throw new InvalidPayloadException(`User in update misses primary key.`);
|
||||
keys.push(await service.updateOne(item[primaryKeyField]!, item, opts));
|
||||
}
|
||||
});
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update many users by primary key
|
||||
*/
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { SchemaOverview } from '@directus/schema/dist/types/overview';
|
||||
import { parseJSON } from '@directus/shared/utils';
|
||||
import { Column } from 'knex-schema-inspector/dist/types/column';
|
||||
import env from '../env';
|
||||
import logger from '../logger';
|
||||
import getLocalType from './get-local-type';
|
||||
import { parseJSON } from './parse-json';
|
||||
|
||||
export default function getDefaultValue(
|
||||
column: SchemaOverview[string]['columns'][string] | Column
|
||||
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
GraphQLType,
|
||||
} from 'graphql';
|
||||
import { GraphQLJSON } from 'graphql-compose';
|
||||
import { GraphQLDate, GraphQLGeoJSON } from '../services/graphql';
|
||||
import { GraphQLDate } from '../services/graphql/types/date';
|
||||
import { GraphQLGeoJSON } from '../services/graphql/types/geojson';
|
||||
import { Type } from '@directus/shared/types';
|
||||
|
||||
export function getGraphQLType(localType: Type | 'alias' | 'unknown'): GraphQLScalarType | GraphQLList<GraphQLType> {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Accountability, Permission, SchemaOverview } from '@directus/shared/types';
|
||||
import { deepMap, parseFilter, parsePreset } from '@directus/shared/utils';
|
||||
import { deepMap, parseFilter, parseJSON, parsePreset } from '@directus/shared/utils';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import hash from 'object-hash';
|
||||
import { getCache, setSystemCache } from '../cache';
|
||||
@@ -10,7 +10,6 @@ import { RolesService } from '../services/roles';
|
||||
import { UsersService } from '../services/users';
|
||||
import { mergePermissions } from '../utils/merge-permissions';
|
||||
import { mergePermissionsForShare } from './merge-permissions-for-share';
|
||||
import { parseJSON } from './parse-json';
|
||||
|
||||
export async function getPermissions(accountability: Accountability, schema: SchemaOverview) {
|
||||
const database = getDatabase();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import SchemaInspector from '@directus/schema';
|
||||
import { Accountability, Filter, SchemaOverview } from '@directus/shared/types';
|
||||
import { toArray } from '@directus/shared/utils';
|
||||
import { parseJSON, toArray } from '@directus/shared/utils';
|
||||
import { Knex } from 'knex';
|
||||
import { mapValues } from 'lodash';
|
||||
import { getCache, setSystemCache } from '../cache';
|
||||
@@ -13,7 +13,6 @@ import logger from '../logger';
|
||||
import { RelationsService } from '../services';
|
||||
import getDefaultValue from './get-default-value';
|
||||
import getLocalType from './get-local-type';
|
||||
import { parseJSON } from './parse-json';
|
||||
|
||||
export async function getSchema(options?: {
|
||||
accountability?: Accountability;
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import { renderFn, get, Scope } from 'micromustache';
|
||||
import { parseJSON } from './parse-json';
|
||||
|
||||
type Mustacheable = string | number | boolean | null | Mustacheable[] | { [key: string]: Mustacheable };
|
||||
type GenericString<T> = T extends string ? string : T;
|
||||
|
||||
function resolveFn(path: string, scope?: Scope): unknown {
|
||||
if (!scope) return undefined;
|
||||
|
||||
const value = get(scope, path);
|
||||
|
||||
return typeof value === 'object' ? JSON.stringify(value) : value;
|
||||
}
|
||||
|
||||
function renderMustache<T extends Mustacheable>(item: T, scope: Scope): GenericString<T> {
|
||||
if (typeof item === 'string') {
|
||||
return renderFn(item, resolveFn, scope, { explicit: true }) as GenericString<T>;
|
||||
} else if (Array.isArray(item)) {
|
||||
return item.map((element) => renderMustache(element, scope)) as GenericString<T>;
|
||||
} else if (typeof item === 'object' && item !== null) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(item).map(([key, value]) => [key, renderMustache(value, scope)])
|
||||
) as GenericString<T>;
|
||||
} else {
|
||||
return item as GenericString<T>;
|
||||
}
|
||||
}
|
||||
|
||||
export function applyOperationOptions(options: Record<string, any>, data: Record<string, any>): Record<string, any> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(options).map(([key, value]) => {
|
||||
if (typeof value === 'string') {
|
||||
const single = value.match(/^\{\{\s*([^}\s]+)\s*\}\}$/);
|
||||
|
||||
if (single !== null) {
|
||||
return [key, get(data, single[1])];
|
||||
}
|
||||
}
|
||||
|
||||
return [key, renderMustache(value, data)];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function optionToObject<T>(option: T): Exclude<T, string> {
|
||||
return typeof option === 'string' ? parseJSON(option) : option;
|
||||
}
|
||||
|
||||
export function optionToString(option: unknown): string {
|
||||
return typeof option === 'object' ? JSON.stringify(option) : String(option);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
/**
|
||||
* Run JSON.parse, but ignore `__proto__` properties. This prevents prototype pollution attacks
|
||||
*/
|
||||
export function parseJSON(input: string): any {
|
||||
if (String(input).includes('__proto__')) {
|
||||
return JSON.parse(input, noproto);
|
||||
}
|
||||
|
||||
return JSON.parse(input);
|
||||
}
|
||||
|
||||
export function noproto<T>(key: string, value: T): T | void {
|
||||
if (key !== '__proto__') {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Accountability, Aggregate, Filter, Query } from '@directus/shared/types';
|
||||
import { parseFilter } from '@directus/shared/utils';
|
||||
import { parseFilter, parseJSON } from '@directus/shared/utils';
|
||||
import { flatten, get, isPlainObject, merge, set } from 'lodash';
|
||||
import logger from '../logger';
|
||||
import { Meta } from '../types';
|
||||
import { parseJSON } from './parse-json';
|
||||
|
||||
export function sanitizeQuery(rawQuery: Record<string, any>, accountability?: Accountability | null): Query {
|
||||
const query: Query = {};
|
||||
|
||||
Reference in New Issue
Block a user